diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index aaed4d9..087125a 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -12,7 +12,7 @@ import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart'; import 'package:flux/features/customers/ui/customer_mobile_upload_screen.dart'; import 'package:flux/features/customers/ui/customers_content.dart'; -import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart'; +import 'package:flux/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart'; import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/master_data/master_data_hub_content.dart'; import 'package:flux/features/master_data/products/ui/products_screen.dart'; @@ -21,11 +21,11 @@ import 'package:flux/features/master_data/staff/ui/staff_screen.dart'; 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/operations/blocs/service_files_bloc.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:flux/features/operations/ui/service_form_screen/service_form_screen.dart'; -import 'package:flux/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart'; -import 'package:flux/features/operations/ui/services_screen.dart'; +import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/operation_form_screen.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart'; +import 'package:flux/features/operations/ui/operations_screen.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -132,7 +132,7 @@ class AppRouter { ), GoRoute( path: '/operations', - builder: (context, state) => const ServicesScreen(), + builder: (context, state) => const OperationsScreen(), ), GoRoute( path: '/customers', @@ -171,14 +171,15 @@ class AppRouter { path: '/operation-form', name: 'operation-form', builder: (context, state) { - final existingService = state.extra as ServiceModel?; - final serviceId = state.uri.queryParameters['serviceId']; + final existingOperation = state.extra as OperationModel?; + final operationId = state.uri.queryParameters['operationId']; return BlocProvider( - create: (context) => - ServiceFilesBloc(serviceId: serviceId ?? existingService?.id), - child: ServiceFormScreen( - serviceId: serviceId ?? existingService?.id, - existingService: existingService, + create: (context) => OperationFilesBloc( + operationId: operationId ?? existingOperation?.id, + ), + child: OperationFormScreen( + operationId: operationId ?? existingOperation?.id, + existingOperation: existingOperation, ), ); }, @@ -186,13 +187,14 @@ class AppRouter { GoRoute( path: '/operation/:id/upload', builder: (context, state) { - final serviceId = state.pathParameters['id']!; - final serviceName = state.uri.queryParameters['name'] ?? 'Pratica'; + final operationId = state.pathParameters['id']!; + final operationName = + state.uri.queryParameters['name'] ?? 'Pratica'; return BlocProvider( - create: (context) => ServiceFilesBloc(serviceId: serviceId), - child: ServiceMobileUploadScreen( - serviceId: serviceId, - serviceName: serviceName, + create: (context) => OperationFilesBloc(operationId: operationId), + child: OperationMobileUploadScreen( + operationId: operationId, + operationName: operationName, ), ); }, diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index 39fcf8c..f2cbf24 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -1,6 +1,6 @@ // lib/ui/common/flux_text_field.dart import 'package:flutter/material.dart'; -import 'package:flutter/operations.dart'; +import 'package:flutter/services.dart'; import 'package:flux/core/theme/theme.dart'; class FluxTextField extends StatefulWidget { diff --git a/lib/features/company/models/company_model.dart b/lib/features/company/models/company_model.dart index 4c5c4aa..97e542a 100644 --- a/lib/features/company/models/company_model.dart +++ b/lib/features/company/models/company_model.dart @@ -263,7 +263,7 @@ extension CompanyLimits on CompanyModel { } } - int get maxServicesPerMonth { + int get maxOperationsPerMonth { switch (subscriptionTier) { case SubscriptionTier.free: return 50; diff --git a/lib/features/customers/ui/customer_search_sheet.dart b/lib/features/customers/ui/customer_search_sheet.dart index 0030995..e93d980 100644 --- a/lib/features/customers/ui/customer_search_sheet.dart +++ b/lib/features/customers/ui/customer_search_sheet.dart @@ -91,7 +91,7 @@ class _CustomerSearchSheetState extends State { child: IconButton( icon: const Icon(Icons.person_add), onPressed: () async { - final servicesCubit = context.read(); + final operationsCubit = context.read(); // Apriamo la dialog passando la query attuale final CustomerModel? nuovoCliente = await showDialog( context: context, @@ -101,7 +101,7 @@ class _CustomerSearchSheetState extends State { ); if (nuovoCliente != null) { - servicesCubit.updateField( + operationsCubit.updateField( customerId: nuovoCliente.id, customerDisplayName: nuovoCliente.nome, ); @@ -180,7 +180,7 @@ class _CustomerSearchSheetState extends State { ), onTap: () { // Salviamo l'ID e il nome formattato nel form dei servizi - context.read().updateField( + context.read().updateField( customerId: customer.id, customerDisplayName: displayName, ); diff --git a/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart b/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart new file mode 100644 index 0000000..5d52d3d --- /dev/null +++ b/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart @@ -0,0 +1,66 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.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 '../../latest_store_operations/bloc/latest_store_operations_events.dart'; +part '../../latest_store_operations/bloc/latest_store_operations_state.dart'; + +class LatestStoreOperationsBloc + extends Bloc { + final _repository = GetIt.I.get(); + + LatestStoreOperationsBloc() + : super( + const LatestStoreOperationsState( + status: LatestStoreOperationsStatus.initial, + ), + ) { + on((event, emit) async { + emit(state.copyWith(status: LatestStoreOperationsStatus.loading)); + try { + // 1. Creiamo uno stream "intermedio" che idrata i dati + final hydratedStream = _repository + .getLastStoreOperationsStream(storeId: event.storeId, limit: 5) + .asyncMap((List rawOperations) async { + // Questo gira ad ogni "scatto" dello stream di Supabase + List fullyHydratedOperations = []; + + for (OperationModel operation in rawOperations) { + // Peschiamo i dati completi (incluso il cliente) + OperationModel fullOperation = await _repository + .fetchOperationById(operation.id!); + fullyHydratedOperations.add(fullOperation); + } + + // Passiamo la lista completa allo step successivo + return fullyHydratedOperations; + }); + + // 2. Ora passiamo lo stream idratato all'emit.forEach + await emit.forEach( + hydratedStream, // Usiamo lo stream modificato! + onData: (List fullyHydratedOperations) { + // Qui ora è tutto sincrono e bellissimo + return state.copyWith( + operations: fullyHydratedOperations, + status: LatestStoreOperationsStatus.success, + ); + }, + onError: (error, stackTrace) => state.copyWith( + status: LatestStoreOperationsStatus.failure, + error: error.toString(), + ), + ); + } catch (e) { + emit( + state.copyWith( + status: LatestStoreOperationsStatus.failure, + error: e.toString(), + ), + ); + } + }); + } +} diff --git a/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart b/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart new file mode 100644 index 0000000..c15c0f8 --- /dev/null +++ b/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart @@ -0,0 +1,17 @@ +part of 'latest_store_operations_bloc.dart'; + +sealed class LatestStoreOperationsEvent extends Equatable { + const LatestStoreOperationsEvent(); + + @override + List get props => []; +} + +class InitLastStoreOperationsEvent extends LatestStoreOperationsEvent { + final String storeId; + + const InitLastStoreOperationsEvent(this.storeId); + + @override + List get props => [storeId]; +} diff --git a/lib/features/home/latest_store_operations/bloc/latest_store_operations_state.dart b/lib/features/home/latest_store_operations/bloc/latest_store_operations_state.dart new file mode 100644 index 0000000..d373848 --- /dev/null +++ b/lib/features/home/latest_store_operations/bloc/latest_store_operations_state.dart @@ -0,0 +1,30 @@ +part of 'latest_store_operations_bloc.dart'; + +enum LatestStoreOperationsStatus { initial, loading, success, failure } + +class LatestStoreOperationsState extends Equatable { + final LatestStoreOperationsStatus status; + final String? error; + final List operations; + + const LatestStoreOperationsState({ + required this.status, + this.error, + this.operations = const [], + }); + + @override + List get props => [status, error, operations]; + + LatestStoreOperationsState copyWith({ + LatestStoreOperationsStatus? status, + String? error, + List? operations, + }) { + return LatestStoreOperationsState( + status: status ?? this.status, + error: error, + operations: operations ?? this.operations, + ); + } +} diff --git a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart new file mode 100644 index 0000000..10123e0 --- /dev/null +++ b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart @@ -0,0 +1,189 @@ +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/theme/theme.dart'; +import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class LatestStoreOperationsCard extends StatelessWidget { + const LatestStoreOperationsCard({super.key}); + + @override + Widget build(BuildContext context) { + final currentStoreId = context.read().state.currentStore?.id; + + return BlocProvider( + // 1. Creiamo il Bloc e facciamo partire subito la query + create: (context) => + LatestStoreOperationsBloc() + ..add(InitLastStoreOperationsEvent(currentStoreId ?? '')), + child: BlocListener( + // 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream! + listenWhen: (previous, current) => + previous.currentStore?.id != current.currentStore?.id, + listener: (context, state) { + if (state.currentStore?.id != null) { + context.read().add( + InitLastStoreOperationsEvent(state.currentStore!.id!), + ); + } + }, + child: _LatestOperationsCardContent(), + ), + ); + } +} + +class _LatestOperationsCardContent extends StatelessWidget { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + const color = Colors.blue; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- HEADER DELLA CARD --- + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.design_services_outlined, + color: color, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextButton( + onPressed: () => context.push('/operations'), + child: Text( + context.l10n.homeLatestOperations, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + + // --- CORPO DELLA CARD (LA LISTA REAL-TIME) --- + Expanded( + child: + BlocBuilder< + LatestStoreOperationsBloc, + LatestStoreOperationsState + >( + builder: (context, state) { + if (state.status == LatestStoreOperationsStatus.loading || + state.status == LatestStoreOperationsStatus.initial) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.status == LatestStoreOperationsStatus.failure) { + return Center( + child: Text( + "Errore di caricamento", + style: TextStyle(color: theme.colorScheme.error), + ), + ); + } + + if (state.operations.isEmpty) { + return Center( + child: Text( + "Nessun servizio recente.", + style: TextStyle( + color: context.secondaryText, + fontStyle: FontStyle.italic, + ), + ), + ); + } + + return ListView.separated( + itemCount: state.operations.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: theme.dividerColor.withValues(alpha: 0.3), + ), + itemBuilder: (context, index) { + final operation = state.operations[index]; + return InkWell( + onTap: () => context.push( + '/operation-form', + extra: operation, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 5, + child: Text( + operation.customerDisplayName ?? + 'Cliente sconosciuto', + style: TextStyle( + fontWeight: FontWeight.w700, + color: context.primaryText, + ), + ), + ), + Expanded( + flex: 5, + child: Text( + operation.number, + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + "${operation.createdAt?.day}/${operation.createdAt?.month}", + style: TextStyle( + color: context.secondaryText, + fontSize: 12, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart deleted file mode 100644 index b80cfd7..0000000 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flux/features/operations/data/services_repository.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:get_it/get_it.dart'; - -part 'latest_store_services_events.dart'; -part 'latest_store_services_state.dart'; - -class LatestStoreServicesBloc - extends Bloc { - final _repository = GetIt.I.get(); - - LatestStoreServicesBloc() - : super( - const LatestStoreServicesState( - status: LatestStoreServicesStatus.initial, - ), - ) { - on((event, emit) async { - emit(state.copyWith(status: LatestStoreServicesStatus.loading)); - try { - // 1. Creiamo uno stream "intermedio" che idrata i dati - final hydratedStream = _repository - .getLastStoreServicesStream(storeId: event.storeId, limit: 5) - .asyncMap((List rawServices) async { - // Questo gira ad ogni "scatto" dello stream di Supabase - List fullyHydratedServices = []; - - for (ServiceModel operation in rawServices) { - // Peschiamo i dati completi (incluso il cliente) - ServiceModel fullService = await _repository.fetchServiceById( - operation.id!, - ); - fullyHydratedServices.add(fullService); - } - - // Passiamo la lista completa allo step successivo - return fullyHydratedServices; - }); - - // 2. Ora passiamo lo stream idratato all'emit.forEach - await emit.forEach( - hydratedStream, // Usiamo lo stream modificato! - onData: (List fullyHydratedServices) { - // Qui ora è tutto sincrono e bellissimo - return state.copyWith( - operations: fullyHydratedServices, - status: LatestStoreServicesStatus.success, - ); - }, - onError: (error, stackTrace) => state.copyWith( - status: LatestStoreServicesStatus.failure, - error: error.toString(), - ), - ); - } catch (e) { - emit( - state.copyWith( - status: LatestStoreServicesStatus.failure, - error: e.toString(), - ), - ); - } - }); - } -} diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart deleted file mode 100644 index b66128d..0000000 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart +++ /dev/null @@ -1,17 +0,0 @@ -part of 'latest_store_services_bloc.dart'; - -sealed class LatestStoreServicesEvent extends Equatable { - const LatestStoreServicesEvent(); - - @override - List get props => []; -} - -class InitLastStoreServicesEvent extends LatestStoreServicesEvent { - final String storeId; - - const InitLastStoreServicesEvent(this.storeId); - - @override - List get props => [storeId]; -} diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart deleted file mode 100644 index 8c9907d..0000000 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of 'latest_store_services_bloc.dart'; - -enum LatestStoreServicesStatus { initial, loading, success, failure } - -class LatestStoreServicesState extends Equatable { - final LatestStoreServicesStatus status; - final String? error; - final List operations; - - const LatestStoreServicesState({ - required this.status, - this.error, - this.operations = const [], - }); - - @override - List get props => [status, error, operations]; - - LatestStoreServicesState copyWith({ - LatestStoreServicesStatus? status, - String? error, - List? operations, - }) { - return LatestStoreServicesState( - status: status ?? this.status, - error: error, - operations: operations ?? this.operations, - ); - } -} diff --git a/lib/features/home/latest_store_services/ui/latest_store_services_card.dart b/lib/features/home/latest_store_services/ui/latest_store_services_card.dart deleted file mode 100644 index bf8556c..0000000 --- a/lib/features/home/latest_store_services/ui/latest_store_services_card.dart +++ /dev/null @@ -1,180 +0,0 @@ -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/theme/theme.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class LatestStoreServicesCard extends StatelessWidget { - const LatestStoreServicesCard({super.key}); - - @override - Widget build(BuildContext context) { - final currentStoreId = context.read().state.currentStore?.id; - - return BlocProvider( - // 1. Creiamo il Bloc e facciamo partire subito la query - create: (context) => - LatestStoreServicesBloc() - ..add(InitLastStoreServicesEvent(currentStoreId ?? '')), - child: BlocListener( - // 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream! - listenWhen: (previous, current) => - previous.currentStore?.id != current.currentStore?.id, - listener: (context, state) { - if (state.currentStore?.id != null) { - context.read().add( - InitLastStoreServicesEvent(state.currentStore!.id!), - ); - } - }, - child: _LatestServicesCardContent(), - ), - ); - } -} - -class _LatestServicesCardContent extends StatelessWidget { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - const color = Colors.blue; - - return Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // --- HEADER DELLA CARD --- - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.design_services_outlined, - color: color, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextButton( - onPressed: () => context.push('/operations'), - child: Text( - context.l10n.homeLatestServices, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: context.primaryText, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - - // --- CORPO DELLA CARD (LA LISTA REAL-TIME) --- - Expanded( - child: BlocBuilder( - builder: (context, state) { - if (state.status == LatestStoreServicesStatus.loading || - state.status == LatestStoreServicesStatus.initial) { - return const Center(child: CircularProgressIndicator()); - } - - if (state.status == LatestStoreServicesStatus.failure) { - return Center( - child: Text( - "Errore di caricamento", - style: TextStyle(color: theme.colorScheme.error), - ), - ); - } - - if (state.operations.isEmpty) { - return Center( - child: Text( - "Nessun servizio recente.", - style: TextStyle( - color: context.secondaryText, - fontStyle: FontStyle.italic, - ), - ), - ); - } - - return ListView.separated( - itemCount: state.operations.length, - separatorBuilder: (context, index) => Divider( - height: 1, - color: theme.dividerColor.withValues(alpha: 0.3), - ), - itemBuilder: (context, index) { - final operation = state.operations[index]; - return InkWell( - onTap: () => - context.push('/operation-form', extra: operation), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 5, - child: Text( - operation.customerDisplayName ?? - 'Cliente sconosciuto', - style: TextStyle( - fontWeight: FontWeight.w700, - color: context.primaryText, - ), - ), - ), - Expanded( - flex: 5, - child: Text( - operation.number, - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.primaryText, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - "${operation.createdAt?.day}/${operation.createdAt?.month}", - style: TextStyle( - color: context.secondaryText, - fontSize: 12, - ), - ), - ], - ), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 69786ee..1c498b2 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/home/latest_store_services/ui/latest_store_services_card.dart'; +import 'package:flux/features/home/latest_store_operations/ui/latest_store_operations_card.dart'; import 'package:flux/features/home/ui/quick_actions_widget.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:go_router/go_router.dart'; @@ -76,10 +76,10 @@ class HomeScreen extends StatelessWidget { color: Colors.green, context: context, ), - LatestStoreServicesCard(), + LatestStoreOperationsCard(), _buildDashboardWidget( - title: context.l10n.homeLatestServiceTickets, + title: context.l10n.homeLatestOperationTickets, icon: Icons.support_agent_outlined, color: Colors.purple, context: context, @@ -181,7 +181,7 @@ class HomeScreen extends StatelessWidget { children: [ QuickActionButton( icon: Icons.add, - label: context.l10n.commonService, + label: context.l10n.commonOperation, color: Colors.blue, onTap: () { // Entriamo nel form! Nessun parametro extra = Nuovo Servizio @@ -191,7 +191,7 @@ class HomeScreen extends StatelessWidget { const SizedBox(width: 12), QuickActionButton( icon: Icons.handyman, - label: context.l10n.homeNewServiceTicket, + label: context.l10n.homeNewOperationTicket, color: Colors.redAccent, onTap: () { // TODO: Quando avrai la rotta per la nuova assistenza diff --git a/lib/features/onboarding/ui/store_onboarding_form.dart b/lib/features/onboarding/ui/store_onboarding_form.dart index 26f5a6e..2408abf 100644 --- a/lib/features/onboarding/ui/store_onboarding_form.dart +++ b/lib/features/onboarding/ui/store_onboarding_form.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/operations.dart'; // <-- IMPORTANTE per i formatter +import 'package:flutter/services.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; diff --git a/lib/features/operations/blocs/operation_files_bloc.dart b/lib/features/operations/blocs/operation_files_bloc.dart new file mode 100644 index 0000000..6407d5a --- /dev/null +++ b/lib/features/operations/blocs/operation_files_bloc.dart @@ -0,0 +1,242 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'operation_files_events.dart'; +part 'operation_files_state.dart'; + +class OperationFilesBloc + extends Bloc { + final _repository = GetIt.I.get(); + final String? operationId; + + OperationFilesBloc({this.operationId}) + : super( + OperationFilesState( + status: OperationFilesStatus.initial, + operationId: operationId, + ), + ) { + on(_onOperationsaved); + on(_onLoadOperationFiles); + on(_onAddOperationFiles); + on(_onUploadOperationFiles); + on(_onUploadMultipleOperationFiles); + on(_onDeleteOperationFiles); + on(_onToggleOperationFileSelection); + // Se il BLoC nasce con un ID, accendiamo subito lo stream! + if (operationId != null) { + add(LoadOperationFilesEvent(operationId: operationId)); + } + } + + FutureOr _onOperationsaved( + OperationsavedEvent event, + Emitter emit, + ) { + // 1. Aggiorniamo l'ID nello stato + // 2. PIALLIAMO i file locali: ormai sono partiti per Supabase! + // Così la UI si pulisce all'istante e aspetta quelli remoti. + emit( + state.copyWith( + operationId: event.operationId, + localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI + ), + ); + + // Lanciamo il caricamento + add(LoadOperationFilesEvent(operationId: event.operationId)); + } + + FutureOr _onLoadOperationFiles( + LoadOperationFilesEvent event, + Emitter emit, + ) async { + // Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato + final currentId = event.operationId ?? state.operationId; + + if (currentId != null) { + emit(state.copyWith(status: OperationFilesStatus.loading)); + + await emit.forEach( + _repository.getOperationFilesStream( + currentId, + ), // <-- Usiamo l'ID corretto! + onData: (data) => state.copyWith( + status: OperationFilesStatus.success, + remoteFiles: data, + ), + onError: (error, stackTrace) => state.copyWith( + status: OperationFilesStatus.failure, + error: error.toString(), + ), + ); + } + } + + void _onAddOperationFiles( + AddOperationFilesEvent event, + Emitter emit, + ) async { + final currentId = state.operationId; + // BIVIO 1: PRATICA NUOVA (Nessun ID) + if (currentId == null) { + // Mettiamo i file nel "parcheggio" locale dello State + final newLocalFiles = event.files.map((file) { + return OperationFileModel( + id: null, + operationId: operationId ?? '', + name: file.name.fileNameWithoutExtension(), + extension: file.name.fileExtension(), + storagePath: '', + fileSize: file.size, + localBytes: file.bytes, + ); + }).toList(); + final List updatedLocalFiles = [ + ...state.localFiles, + ...newLocalFiles, + ]; + emit( + state.copyWith( + localFiles: updatedLocalFiles, + status: OperationFilesStatus.success, + ), + ); + return; + } + + // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID) + emit(state.copyWith(status: OperationFilesStatus.uploading)); + try { + // Logica identica a quella che abbiamo fatto per i clienti + for (var file in event.files) { + await _repository.uploadAndRegisterOperationFile( + operationId: operationId!, + pickedFile: file, + ); + } + emit(state.copyWith(status: OperationFilesStatus.success)); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: e.toString(), + ), + ); + } + } + + FutureOr _onUploadOperationFiles( + UploadOperationFilesEvent event, + Emitter emit, + ) async { + if (event.pickedFiles == null && event.photos == null) return; + if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return; + + // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID + emit(state.copyWith(status: OperationFilesStatus.uploading)); + try { + // Logica identica a quella che abbiamo fatto per i clienti + if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) { + for (var file in event.pickedFiles!) { + await _repository.uploadAndRegisterOperationFile( + operationId: state.operationId!, + pickedFile: file, + ); + } + } + emit(state.copyWith(status: OperationFilesStatus.success)); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: e.toString(), + ), + ); + } + } + + FutureOr _onUploadMultipleOperationFiles( + UploadMultipleOperationFilesEvent event, + Emitter emit, + ) async { + if (event.files.isEmpty) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: "Nessun file selezionato", + ), + ); + return; + } + emit(state.copyWith(status: OperationFilesStatus.uploading, error: null)); + try { + // 2. Creiamo una lista di "Promesse" (Futures) per il repository + final List> uploadTasks = []; + for (var file in event.files) { + // Aggiungiamo il task alla lista, ma NON usiamo await qui dentro! + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: state.operationId!, + pickedFile: file, + ), + ); + } + // 3. ESECUZIONE PARALLELA! + // Aspettiamo che tutti i file siano caricati contemporaneamente. + await Future.wait(uploadTasks); + // 4. GRAN FINALE: Tutto caricato, emettiamo il success! + emit(state.copyWith(status: OperationFilesStatus.success)); + } catch (e) { + // Se anche un solo file fallisce, catturiamo l'errore + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: "Errore durante l'upload multiplo: $e", + ), + ); + } + } + + FutureOr _onDeleteOperationFiles( + DeleteOperationFilesEvent event, + Emitter emit, + ) async { + emit(state.copyWith(status: OperationFilesStatus.loading)); + try { + await _repository.deleteOperationFiles(state.selectedFiles); + emit( + state.copyWith(status: OperationFilesStatus.success, selectedFiles: []), + ); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: e.toString(), + ), + ); + } + } + + FutureOr _onToggleOperationFileSelection( + ToggleOperationFileSelectionEvent event, + Emitter emit, + ) { + List selectedFiles = List.from(state.selectedFiles); + if (selectedFiles.contains(event.file)) { + selectedFiles.remove(event.file); + } else { + selectedFiles.add(event.file); + } + emit(state.copyWith(selectedFiles: selectedFiles)); + } +} diff --git a/lib/features/operations/blocs/operation_files_events.dart b/lib/features/operations/blocs/operation_files_events.dart new file mode 100644 index 0000000..4542902 --- /dev/null +++ b/lib/features/operations/blocs/operation_files_events.dart @@ -0,0 +1,56 @@ +part of 'operation_files_bloc.dart'; + +abstract class OperationFilesEvent extends Equatable { + const OperationFilesEvent(); + + @override + List get props => []; +} + +class OperationsavedEvent extends OperationFilesEvent { + final String operationId; + const OperationsavedEvent(this.operationId); + + @override + List get props => [operationId]; +} + +class LoadOperationFilesEvent extends OperationFilesEvent { + final String? operationId; + final OperationModel? operation; + const LoadOperationFilesEvent({this.operationId, this.operation}); + + @override + List get props => [operationId, operation]; +} + +class AddOperationFilesEvent extends OperationFilesEvent { + final List files; + const AddOperationFilesEvent(this.files); + + @override + List get props => [files]; +} + +class UploadOperationFilesEvent extends OperationFilesEvent { + final List? pickedFiles; + final List? photos; + const UploadOperationFilesEvent({this.pickedFiles, this.photos}); + + @override + List get props => [pickedFiles, photos]; +} + +class UploadMultipleOperationFilesEvent extends OperationFilesEvent { + final List files; + const UploadMultipleOperationFilesEvent(this.files); + @override + List get props => [files]; +} + +class DeleteOperationFilesEvent extends OperationFilesEvent {} + +class ToggleOperationFileSelectionEvent extends OperationFilesEvent { + final OperationFileModel file; + const ToggleOperationFileSelectionEvent(this.file); +} diff --git a/lib/features/operations/blocs/operation_files_state.dart b/lib/features/operations/blocs/operation_files_state.dart new file mode 100644 index 0000000..8ea5eb3 --- /dev/null +++ b/lib/features/operations/blocs/operation_files_state.dart @@ -0,0 +1,52 @@ +part of 'operation_files_bloc.dart'; + +enum OperationFilesStatus { initial, loading, uploading, success, failure } + +class OperationFilesState extends Equatable { + const OperationFilesState({ + this.operationId, + required this.status, + this.error, + this.localFiles = const [], + this.remoteFiles = const [], + this.selectedFiles = const [], + }); + + final String? operationId; + final OperationFilesStatus status; + final String? error; + final List localFiles; + final List remoteFiles; + + final List selectedFiles; + + @override + List get props => [ + operationId, + status, + error, + localFiles, + remoteFiles, + selectedFiles, + ]; + + List get allFiles => [...remoteFiles, ...localFiles]; + + OperationFilesState copyWith({ + String? operationId, + OperationFilesStatus? status, + String? error, + List? localFiles, + List? remoteFiles, + List? selectedFiles, + }) { + return OperationFilesState( + operationId: operationId ?? this.operationId, + status: status ?? this.status, + error: error, + localFiles: localFiles ?? this.localFiles, + remoteFiles: remoteFiles ?? this.remoteFiles, + selectedFiles: selectedFiles ?? this.selectedFiles, + ); + } +} diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index 036a32f..f66e1b0 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -4,50 +4,51 @@ 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/utils/extensions.dart'; -import 'package:flux/features/operations/data/services_repository.dart'; -import 'package:flux/features/operations/models/energy_service_model.dart'; -import 'package:flux/features/operations/models/entertainment_service_model.dart'; -import 'package:flux/features/operations/models/fin_service_model.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; -import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/operations/models/energy_operation_model.dart'; +import 'package:flux/features/operations/models/entertainment_operation_model.dart'; +import 'package:flux/features/operations/models/fin_operation_model.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; import 'package:get_it/get_it.dart'; import 'package:collection/collection.dart'; -part 'services_state.dart'; +part 'operations_state.dart'; -class ServicesCubit extends Cubit { - final ServicesRepository _repository = GetIt.I(); +class OperationsCubit extends Cubit { + final OperationsRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); - ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial)); + OperationsCubit() + : super(const OperationsState(status: OperationsStatus.initial)); // --- CARICAMENTO E PAGINAZIONE --- - Future loadServices({bool refresh = false}) async { + Future loadOperations({bool refresh = false}) async { // Se stiamo già caricando, evitiamo chiamate doppie - if (state.status == ServicesStatus.loading) return; + if (state.status == OperationsStatus.loading) return; // Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo if (!refresh && state.hasReachedMax) return; emit( state.copyWith( - status: ServicesStatus.loading, + status: OperationsStatus.loading, errorMessage: null, // Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading - allServices: refresh ? [] : state.allServices, + allOperations: refresh ? [] : state.allOperations, hasReachedMax: refresh ? false : state.hasReachedMax, ), ); try { - final currentOffset = refresh ? 0 : state.allServices.length; + 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 newServices = await _repository.fetchServices( + final newOperations = await _repository.fetchOperations( companyId: companyId, offset: currentOffset, limit: 50, @@ -56,21 +57,21 @@ class ServicesCubit extends Cubit { ); // Se ricevi meno record del limite, significa che non ce ne sono altri sul DB - final bool reachedMax = newServices.length < 50; + final bool reachedMax = newOperations.length < 50; emit( state.copyWith( - status: ServicesStatus.ready, - allServices: refresh - ? newServices - : [...state.allServices, ...newServices], + status: OperationsStatus.ready, + allOperations: refresh + ? newOperations + : [...state.allOperations, ...newOperations], hasReachedMax: reachedMax, ), ); } catch (e) { emit( state.copyWith( - status: ServicesStatus.failure, + status: OperationsStatus.failure, errorMessage: "Errore nel caricamento servizi: $e", ), ); @@ -87,51 +88,51 @@ class ServicesCubit extends Cubit { dateRange: range ?? state.dateRange, ), ); - loadServices(refresh: true); + loadOperations(refresh: true); } /// Pulisce tutti i filtri void clearFilters() { emit(state.copyWith(query: '', dateRange: null)); - loadServices(refresh: true); + loadOperations(refresh: true); } // --- GESTIONE BOZZA (DRAFT) --- /// Inizializza un nuovo servizio o ne carica uno esistente per la modifica - void initServiceForm({ - ServiceModel? existingService, - String? serviceId, + void initOperationForm({ + OperationModel? existingOperation, + String? operationId, }) async { - if (existingService != null) { + if (existingOperation != null) { emit( state.copyWith( - currentService: existingService, - status: ServicesStatus.ready, + currentOperation: existingOperation, + status: OperationsStatus.ready, ), ); - } else if (serviceId != null) { - ServiceModel? serviceModel = state.allServices.firstWhereOrNull( - (s) => s.id == serviceId, + } else if (operationId != null) { + OperationModel? operationModel = state.allOperations.firstWhereOrNull( + (s) => s.id == operationId, ); - serviceModel ??= await _repository.fetchServiceById(serviceId); + operationModel ??= await _repository.fetchOperationById(operationId); emit( state.copyWith( - currentService: serviceModel, - status: ServicesStatus.ready, + currentOperation: operationModel, + status: OperationsStatus.ready, ), ); } else { // Crea un template vuoto con lo store di default (se disponibile) emit( state.copyWith( - currentService: ServiceModel( + currentOperation: OperationModel( storeId: _sessionCubit.state.currentStore?.id ?? '', number: '', // Sarà compilato dall'utente createdAt: DateTime.now(), companyId: _sessionCubit.state.company!.id!, ), - status: ServicesStatus.ready, + status: OperationsStatus.ready, ), ); } @@ -151,9 +152,9 @@ class ServicesCubit extends Cubit { String? customerId, String? customerDisplayName, }) { - if (state.currentService == null) return; + if (state.currentOperation == null) return; - final updated = state.currentService!.copyWith( + final updated = state.currentOperation!.copyWith( al: al, mnp: mnp, nip: nip, @@ -167,34 +168,38 @@ class ServicesCubit extends Cubit { customerDisplayName: customerDisplayName, ); - emit(state.copyWith(currentService: updated)); + emit(state.copyWith(currentOperation: updated)); } // --- GESTIONE MODULI COMPLESSI --- - void updateEnergyServices(List energyList) { + void updateEnergyOperations(List energyList) { emit( state.copyWith( - currentService: state.currentService?.copyWith( - energyServices: energyList, + currentOperation: state.currentOperation?.copyWith( + energyOperations: energyList, ), ), ); } - void updateFinServices(List finList) { + void updateFinOperations(List finList) { emit( state.copyWith( - currentService: state.currentService?.copyWith(finServices: finList), + currentOperation: state.currentOperation?.copyWith( + finOperations: finList, + ), ), ); } - void updateEntertainmentServices(List entList) { + void updateEntertainmentOperations( + List entList, + ) { emit( state.copyWith( - currentService: state.currentService?.copyWith( - entertainmentServices: entList, + currentOperation: state.currentOperation?.copyWith( + entertainmentOperations: entList, ), ), ); @@ -202,36 +207,40 @@ class ServicesCubit extends Cubit { // --- PERSISTENZA --- - Future saveCurrentService({ + Future saveCurrentOperation({ required bool isBozza, bool shouldPop = true, - List? files, + List? files, }) async { - if (state.currentService == null) return; + if (state.currentOperation == null) return; - emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null)); + emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null)); try { // 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente - final serviceToSave = state.currentService!.copyWith( + final operationToSave = state.currentOperation!.copyWith( isBozza: isBozza, files: files, ); // 2. Salvataggio corazzato - final updatedService = await _repository.saveFullService(serviceToSave); + final updatedOperation = await _repository.saveFullOperation( + operationToSave, + ); // 3. Reset e ricaricamento emit( state.copyWith( - status: shouldPop ? ServicesStatus.saved : ServicesStatus.savedNoPop, - currentService: shouldPop ? null : updatedService, + status: shouldPop + ? OperationsStatus.saved + : OperationsStatus.savedNoPop, + currentOperation: shouldPop ? null : updatedOperation, ), ); - await loadServices(refresh: true); + await loadOperations(refresh: true); } catch (e) { emit( state.copyWith( - status: ServicesStatus.failure, + status: OperationsStatus.failure, errorMessage: e.toString(), ), ); @@ -242,9 +251,9 @@ class ServicesCubit extends Cubit { void addAttachments(List files) { final newAttachments = files.map((file) { - return ServiceFileModel( + return OperationFileModel( id: null, // Meglio null se non è su DB - serviceId: state.currentService?.id ?? '', + operationId: state.currentOperation?.id ?? '', name: file.name.fileNameWithoutExtension(), extension: file.name.fileExtension(), storagePath: '', @@ -255,44 +264,46 @@ class ServicesCubit extends Cubit { }).toList(); // Creiamo una nuova lista pulita - final List updatedList = [ - ...(state.currentService?.files ?? []), + final List updatedList = [ + ...(state.currentOperation?.files ?? []), ...newAttachments, ]; - // Emettiamo lo stato assicurandoci che il ServiceModel venga clonato - if (state.currentService != null) { + // Emettiamo lo stato assicurandoci che il OperationModel venga clonato + if (state.currentOperation != null) { emit( state.copyWith( - currentService: state.currentService!.copyWith(files: updatedList), + currentOperation: state.currentOperation!.copyWith( + files: updatedList, + ), ), ); } } void removeAttachment(int index) { - if (state.currentService == null) return; + if (state.currentOperation == null) return; - final updatedList = List.from( - state.currentService!.files, + final updatedList = List.from( + state.currentOperation!.files, ); updatedList.removeAt(index); emit( state.copyWith( - currentService: state.currentService?.copyWith(files: updatedList), + currentOperation: state.currentOperation?.copyWith(files: updatedList), ), ); } - void saveAndCopyFileToCustomer(List selectedFiles) async { - final currentService = state.currentService; + void saveAndCopyFileToCustomer(List selectedFiles) async { + final currentOperation = state.currentOperation; // 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare - if (currentService == null || currentService.customerId == null) { + if (currentOperation == null || currentOperation.customerId == null) { emit( state.copyWith( - status: ServicesStatus.failure, + status: OperationsStatus.failure, errorMessage: "Impossibile copiare: nessun cliente associato alla pratica.", ), @@ -300,19 +311,21 @@ class ServicesCubit extends Cubit { return; } - emit(state.copyWith(status: ServicesStatus.loading)); + emit(state.copyWith(status: OperationsStatus.loading)); try { // 2. SALVATAGGIO CORAZZATO // Chiamiamo il repo e otteniamo la pratica con TUTTI i file ora dotati di ID e storagePath - final updatedService = await _repository.saveFullService(currentService); + final updatedOperation = await _repository.saveFullOperation( + currentOperation, + ); // 3. COPIA RELAZIONALE // Per ogni file che l'utente ha selezionato nella UI, cerchiamo la sua versione // "ufficiale" (quella con lo storagePath) nel modello appena tornato dal DB. for (var selectedFile in selectedFiles) { // Cerchiamo il match nel modello aggiornato - final persistedFile = updatedService.files.firstWhere( + final persistedFile = updatedOperation.files.firstWhere( (f) => f.name == selectedFile.name && f.extension == selectedFile.extension, @@ -324,7 +337,7 @@ class ServicesCubit extends Cubit { // Creiamo il link nel database del cliente await _repository.copyFileToCustomer( file: persistedFile, - customerId: currentService.customerId!, + customerId: currentOperation.customerId!, ); } @@ -332,14 +345,14 @@ class ServicesCubit extends Cubit { // Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti" emit( state.copyWith( - status: ServicesStatus.success, - currentService: updatedService, + status: OperationsStatus.success, + currentOperation: updatedOperation, ), ); } catch (e) { emit( state.copyWith( - status: ServicesStatus.failure, + status: OperationsStatus.failure, errorMessage: "Errore durante il salvataggio e copia: $e", ), ); diff --git a/lib/features/operations/blocs/services_state.dart b/lib/features/operations/blocs/operations_state.dart similarity index 60% rename from lib/features/operations/blocs/services_state.dart rename to lib/features/operations/blocs/operations_state.dart index 5af7130..97276ad 100644 --- a/lib/features/operations/blocs/services_state.dart +++ b/lib/features/operations/blocs/operations_state.dart @@ -1,6 +1,6 @@ part of 'operations_cubit.dart'; -enum ServicesStatus { +enum OperationsStatus { initial, loading, ready, @@ -11,20 +11,20 @@ enum ServicesStatus { failure, } -class ServicesState extends Equatable { - final ServicesStatus status; - final List allServices; - final ServiceModel? currentService; // La bozza che stiamo editando +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 ServicesState({ + const OperationsState({ required this.status, - this.allServices = const [], - this.currentService, + this.allOperations = const [], + this.currentOperation, this.errorMessage, this.query = '', this.dateRange, @@ -32,20 +32,20 @@ class ServicesState extends Equatable { this.isSavingDraft = false, }); - ServicesState copyWith({ - ServicesStatus? status, - List? allServices, - ServiceModel? currentService, + OperationsState copyWith({ + OperationsStatus? status, + List? allOperations, + OperationModel? currentOperation, String? errorMessage, String? query, DateTimeRange? dateRange, bool? hasReachedMax, bool? isSavingDraft, }) { - return ServicesState( + return OperationsState( status: status ?? this.status, - allServices: allServices ?? this.allServices, - currentService: currentService ?? this.currentService, + allOperations: allOperations ?? this.allOperations, + currentOperation: currentOperation ?? this.currentOperation, errorMessage: errorMessage, query: query ?? this.query, dateRange: dateRange ?? this.dateRange, @@ -57,8 +57,8 @@ class ServicesState extends Equatable { @override List get props => [ status, - allServices, - currentService, + allOperations, + currentOperation, errorMessage, query, dateRange, diff --git a/lib/features/operations/blocs/service_files_bloc.dart b/lib/features/operations/blocs/service_files_bloc.dart deleted file mode 100644 index f34f86d..0000000 --- a/lib/features/operations/blocs/service_files_bloc.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:file_picker/file_picker.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/operations/data/services_repository.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:get_it/get_it.dart'; - -part 'service_files_events.dart'; -part 'service_files_state.dart'; - -class ServiceFilesBloc extends Bloc { - final _repository = GetIt.I.get(); - final String? serviceId; - - ServiceFilesBloc({this.serviceId}) - : super( - ServiceFilesState( - status: ServiceFilesStatus.initial, - serviceId: serviceId, - ), - ) { - on(_onServiceSaved); - on(_onLoadServiceFiles); - on(_onAddServiceFiles); - on(_onUploadServiceFiles); - on(_onUploadMultipleServiceFiles); - on(_onDeleteServiceFiles); - on(_onToggleServiceFileSelection); - // Se il BLoC nasce con un ID, accendiamo subito lo stream! - if (serviceId != null) { - add(LoadServiceFilesEvent(serviceId: serviceId)); - } - } - - FutureOr _onServiceSaved( - ServiceSavedEvent event, - Emitter emit, - ) { - // 1. Aggiorniamo l'ID nello stato - // 2. PIALLIAMO i file locali: ormai sono partiti per Supabase! - // Così la UI si pulisce all'istante e aspetta quelli remoti. - emit( - state.copyWith( - serviceId: event.serviceId, - localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI - ), - ); - - // Lanciamo il caricamento - add(LoadServiceFilesEvent(serviceId: event.serviceId)); - } - - FutureOr _onLoadServiceFiles( - LoadServiceFilesEvent event, - Emitter emit, - ) async { - // Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato - final currentId = event.serviceId ?? state.serviceId; - - if (currentId != null) { - emit(state.copyWith(status: ServiceFilesStatus.loading)); - - await emit.forEach( - _repository.getServiceFilesStream( - currentId, - ), // <-- Usiamo l'ID corretto! - onData: (data) => state.copyWith( - status: ServiceFilesStatus.success, - remoteFiles: data, - ), - onError: (error, stackTrace) => state.copyWith( - status: ServiceFilesStatus.failure, - error: error.toString(), - ), - ); - } - } - - void _onAddServiceFiles( - AddServiceFilesEvent event, - Emitter emit, - ) async { - final currentId = state.serviceId; - // BIVIO 1: PRATICA NUOVA (Nessun ID) - if (currentId == null) { - // Mettiamo i file nel "parcheggio" locale dello State - final newLocalFiles = event.files.map((file) { - return ServiceFileModel( - id: null, - serviceId: serviceId ?? '', - name: file.name.fileNameWithoutExtension(), - extension: file.name.fileExtension(), - storagePath: '', - fileSize: file.size, - localBytes: file.bytes, - ); - }).toList(); - final List updatedLocalFiles = [ - ...state.localFiles, - ...newLocalFiles, - ]; - emit( - state.copyWith( - localFiles: updatedLocalFiles, - status: ServiceFilesStatus.success, - ), - ); - return; - } - - // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID) - emit(state.copyWith(status: ServiceFilesStatus.uploading)); - try { - // Logica identica a quella che abbiamo fatto per i clienti - for (var file in event.files) { - await _repository.uploadAndRegisterServiceFile( - serviceId: serviceId!, - pickedFile: file, - ); - } - emit(state.copyWith(status: ServiceFilesStatus.success)); - } catch (e) { - emit( - state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()), - ); - } - } - - FutureOr _onUploadServiceFiles( - UploadServiceFilesEvent event, - Emitter emit, - ) async { - if (event.pickedFiles == null && event.photos == null) return; - if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return; - - // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID - emit(state.copyWith(status: ServiceFilesStatus.uploading)); - try { - // Logica identica a quella che abbiamo fatto per i clienti - if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) { - for (var file in event.pickedFiles!) { - await _repository.uploadAndRegisterServiceFile( - serviceId: state.serviceId!, - pickedFile: file, - ); - } - } - emit(state.copyWith(status: ServiceFilesStatus.success)); - } catch (e) { - emit( - state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()), - ); - } - } - - FutureOr _onUploadMultipleServiceFiles( - UploadMultipleServiceFilesEvent event, - Emitter emit, - ) async { - if (event.files.isEmpty) { - emit( - state.copyWith( - status: ServiceFilesStatus.failure, - error: "Nessun file selezionato", - ), - ); - return; - } - emit(state.copyWith(status: ServiceFilesStatus.uploading, error: null)); - try { - // 2. Creiamo una lista di "Promesse" (Futures) per il repository - final List> uploadTasks = []; - for (var file in event.files) { - // Aggiungiamo il task alla lista, ma NON usiamo await qui dentro! - uploadTasks.add( - _repository.uploadAndRegisterServiceFile( - serviceId: state.serviceId!, - pickedFile: file, - ), - ); - } - // 3. ESECUZIONE PARALLELA! - // Aspettiamo che tutti i file siano caricati contemporaneamente. - await Future.wait(uploadTasks); - // 4. GRAN FINALE: Tutto caricato, emettiamo il success! - emit(state.copyWith(status: ServiceFilesStatus.success)); - } catch (e) { - // Se anche un solo file fallisce, catturiamo l'errore - emit( - state.copyWith( - status: ServiceFilesStatus.failure, - error: "Errore durante l'upload multiplo: $e", - ), - ); - } - } - - FutureOr _onDeleteServiceFiles( - DeleteServiceFilesEvent event, - Emitter emit, - ) async { - emit(state.copyWith(status: ServiceFilesStatus.loading)); - try { - await _repository.deleteServiceFiles(state.selectedFiles); - emit( - state.copyWith(status: ServiceFilesStatus.success, selectedFiles: []), - ); - } catch (e) { - emit( - state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()), - ); - } - } - - FutureOr _onToggleServiceFileSelection( - ToggleServiceFileSelectionEvent event, - Emitter emit, - ) { - List selectedFiles = List.from(state.selectedFiles); - if (selectedFiles.contains(event.file)) { - selectedFiles.remove(event.file); - } else { - selectedFiles.add(event.file); - } - emit(state.copyWith(selectedFiles: selectedFiles)); - } -} diff --git a/lib/features/operations/blocs/service_files_events.dart b/lib/features/operations/blocs/service_files_events.dart deleted file mode 100644 index cb02eb2..0000000 --- a/lib/features/operations/blocs/service_files_events.dart +++ /dev/null @@ -1,56 +0,0 @@ -part of 'service_files_bloc.dart'; - -abstract class ServiceFilesEvent extends Equatable { - const ServiceFilesEvent(); - - @override - List get props => []; -} - -class ServiceSavedEvent extends ServiceFilesEvent { - final String serviceId; - const ServiceSavedEvent(this.serviceId); - - @override - List get props => [serviceId]; -} - -class LoadServiceFilesEvent extends ServiceFilesEvent { - final String? serviceId; - final ServiceModel? operation; - const LoadServiceFilesEvent({this.serviceId, this.operation}); - - @override - List get props => [serviceId, operation]; -} - -class AddServiceFilesEvent extends ServiceFilesEvent { - final List files; - const AddServiceFilesEvent(this.files); - - @override - List get props => [files]; -} - -class UploadServiceFilesEvent extends ServiceFilesEvent { - final List? pickedFiles; - final List? photos; - const UploadServiceFilesEvent({this.pickedFiles, this.photos}); - - @override - List get props => [pickedFiles, photos]; -} - -class UploadMultipleServiceFilesEvent extends ServiceFilesEvent { - final List files; - const UploadMultipleServiceFilesEvent(this.files); - @override - List get props => [files]; -} - -class DeleteServiceFilesEvent extends ServiceFilesEvent {} - -class ToggleServiceFileSelectionEvent extends ServiceFilesEvent { - final ServiceFileModel file; - const ToggleServiceFileSelectionEvent(this.file); -} diff --git a/lib/features/operations/blocs/service_files_state.dart b/lib/features/operations/blocs/service_files_state.dart deleted file mode 100644 index f39a133..0000000 --- a/lib/features/operations/blocs/service_files_state.dart +++ /dev/null @@ -1,52 +0,0 @@ -part of 'service_files_bloc.dart'; - -enum ServiceFilesStatus { initial, loading, uploading, success, failure } - -class ServiceFilesState extends Equatable { - const ServiceFilesState({ - this.serviceId, - required this.status, - this.error, - this.localFiles = const [], - this.remoteFiles = const [], - this.selectedFiles = const [], - }); - - final String? serviceId; - final ServiceFilesStatus status; - final String? error; - final List localFiles; - final List remoteFiles; - - final List selectedFiles; - - @override - List get props => [ - serviceId, - status, - error, - localFiles, - remoteFiles, - selectedFiles, - ]; - - List get allFiles => [...remoteFiles, ...localFiles]; - - ServiceFilesState copyWith({ - String? serviceId, - ServiceFilesStatus? status, - String? error, - List? localFiles, - List? remoteFiles, - List? selectedFiles, - }) { - return ServiceFilesState( - serviceId: serviceId ?? this.serviceId, - status: status ?? this.status, - error: error, - localFiles: localFiles ?? this.localFiles, - remoteFiles: remoteFiles ?? this.remoteFiles, - selectedFiles: selectedFiles ?? this.selectedFiles, - ); - } -} diff --git a/lib/features/operations/data/services_repository.dart b/lib/features/operations/data/operations_repository.dart similarity index 75% rename from lib/features/operations/data/services_repository.dart rename to lib/features/operations/data/operations_repository.dart index f0f8233..5d496d4 100644 --- a/lib/features/operations/data/services_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -4,40 +4,40 @@ import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import '../models/service_model.dart'; +import '../models/operation_model.dart'; -class ServicesRepository { +class OperationsRepository { final _supabase = Supabase.instance.client; final companyId = GetIt.I.get().state.company!.id; final CustomerRepository _customerRepository = GetIt.I(); // --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO --- - Future fetchServiceById(String id) async { + Future fetchOperationById(String id) async { try { final response = await _supabase .from('operation') .select(''' *, customer(nome), - energy_service(*), - fin_service(*), - entertainment_service(*), - service_file(*) + energy_operation(*), + fin_operation(*), + entertainment_operation(*), + operation_file(*) ''') .eq('id', id) .single(); - return ServiceModel.fromMap(response); + return OperationModel.fromMap(response); } catch (e) { throw Exception('Errore nel caricamento del servizio: $e'); } } // --- RECUPERO PAGINATO CON FILTRI E JOIN --- - Future> fetchServices({ + Future> fetchOperations({ required String companyId, required int offset, int limit = 50, @@ -51,10 +51,10 @@ class ServicesRepository { .select(''' *, customer(nome), - energy_service(*), - fin_service(*), - entertainment_service(*), - service_file(*) + energy_operation(*), + fin_operation(*), + entertainment_operation(*), + operation_file(*) ''') .eq('company_id', companyId); @@ -77,14 +77,14 @@ class ServicesRepository { .range(offset, offset + limit - 1); return (response as List) - .map((map) => ServiceModel.fromMap(map)) + .map((map) => OperationModel.fromMap(map)) .toList(); } catch (e) { throw Exception('Errore nel caricamento servizi: $e'); } } - Stream> getLastStoreServicesStream({ + Stream> getLastStoreOperationsStream({ required String storeId, required int limit, }) { @@ -96,32 +96,32 @@ class ServicesRepository { .limit(limit) .map( (listOfMaps) => - listOfMaps.map((map) => ServiceModel.fromMap(map)).toList(), + listOfMaps.map((map) => OperationModel.fromMap(map)).toList(), ); } // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- - Future saveFullService(ServiceModel operation) async { + Future saveFullOperation(OperationModel operation) async { try { // 1. Upsert del record principale - final serviceData = await _supabase + final operationData = await _supabase .from('operation') .upsert(operation.toMap()) .select() .single(); - final String newId = serviceData['id']; + final String newId = operationData['id']; // 2. MODIFICA: Pulizia atomica dei figli // Se stiamo modificando (id != null), resettiamo le tabelle collegate if (operation.id != null) { await Future.wait([ - _supabase.from('energy_service').delete().eq('service_id', newId), - _supabase.from('fin_service').delete().eq('service_id', newId), + _supabase.from('energy_operation').delete().eq('operation_id', newId), + _supabase.from('fin_operation').delete().eq('operation_id', newId), _supabase - .from('entertainment_service') + .from('entertainment_operation') .delete() - .eq('service_id', newId), + .eq('operation_id', newId), // Aggiungi qui eventuali altre tabelle pivot o file ]); } @@ -129,37 +129,37 @@ class ServicesRepository { // 3. Inserimento dei moduli in parallelo per velocità final List insertTasks = []; - if (operation.energyServices.isNotEmpty) { + if (operation.energyOperations.isNotEmpty) { insertTasks.add( _supabase - .from('energy_service') + .from('energy_operation') .insert( - operation.energyServices - .map((item) => item.copyWith(serviceId: newId).toMap()) + operation.energyOperations + .map((item) => item.copyWith(operationId: newId).toMap()) .toList(), ), ); } - if (operation.finServices.isNotEmpty) { + if (operation.finOperations.isNotEmpty) { insertTasks.add( _supabase - .from('fin_service') + .from('fin_operation') .insert( - operation.finServices - .map((item) => item.copyWith(serviceId: newId).toMap()) + operation.finOperations + .map((item) => item.copyWith(operationId: newId).toMap()) .toList(), ), ); } - if (operation.entertainmentServices.isNotEmpty) { + if (operation.entertainmentOperations.isNotEmpty) { insertTasks.add( _supabase - .from('entertainment_service') + .from('entertainment_operation') .insert( - operation.entertainmentServices - .map((item) => item.copyWith(serviceId: newId).toMap()) + operation.entertainmentOperations + .map((item) => item.copyWith(operationId: newId).toMap()) .toList(), ), ); @@ -186,7 +186,7 @@ class ServicesRepository { : 'image/${file.extension}'; final fileToSave = file.copyWith( - serviceId: newId, + operationId: newId, storagePath: storagePath, ); @@ -202,7 +202,7 @@ class ServicesRepository { ); // B. Inserimento riga nel DB relazionale - await _supabase.from('service_file').insert(fileToSave.toMap()); + await _supabase.from('operation_file').insert(fileToSave.toMap()); } uploadTasks.add(uploadAndLink()); @@ -214,20 +214,20 @@ class ServicesRepository { // 5. GRAN FINALE: RECUPERO DEL MODELLO COMPLETO E AGGIORNATO // Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati - // (inclusi quelli della tabella service_file appena inseriti) - final updatedServiceData = await _supabase + // (inclusi quelli della tabella operation_file appena inseriti) + final updatedOperationData = await _supabase .from('operation') .select(''' *, - energy_service(*), - fin_service(*), - entertainment_service(*), - service_file(*) + energy_operation(*), + fin_operation(*), + entertainment_operation(*), + operation_file(*) ''') .eq('id', newId) .single(); - return ServiceModel.fromMap(updatedServiceData); + return OperationModel.fromMap(updatedOperationData); } catch (e) { // Qui potresti aggiungere una logica di "rollback manuale" se necessario throw Exception('Errore durante il salvataggio corazzato: $e'); @@ -235,7 +235,7 @@ class ServicesRepository { } // --- ELIMINAZIONE --- - Future deleteService(String id) async { + Future deleteOperation(String id) async { try { await _supabase.from('operation').delete().eq('id', id); } catch (e) { @@ -249,7 +249,7 @@ class ServicesRepository { // Cerchiamo i tipi più frequenti associati ai servizi di questa company // Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id final response = await _supabase - .from('entertainment_service') + .from('entertainment_operation') .select('type, operation!inner(store!inner(company_id))') .eq('operation.store.company_id', companyId) .limit(100); // Prendiamo un campione @@ -276,20 +276,20 @@ class ServicesRepository { } /// Ascolta in tempo reale i file caricati per una pratica - Stream> getServiceFilesStream(String serviceId) { + Stream> getOperationFilesStream(String operationId) { return _supabase - .from('service_file') + .from('operation_file') .stream(primaryKey: ['id']) - .eq('service_id', serviceId) + .eq('operation_id', operationId) .order('created_at', ascending: false) .map( (listOfMaps) => - listOfMaps.map((map) => ServiceFileModel.fromMap(map)).toList(), + listOfMaps.map((map) => OperationFileModel.fromMap(map)).toList(), ); } - Future uploadAndRegisterServiceFile({ - required String serviceId, + Future uploadAndRegisterOperationFile({ + required String operationId, required PlatformFile pickedFile, }) async { final cleanFileName = pickedFile.name.replaceAll( @@ -297,10 +297,10 @@ class ServicesRepository { '_', ); final storagePath = - '$companyId/operations/$serviceId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; + '$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; final int fileSize = pickedFile.size; - final fileToSave = ServiceFileModel( - serviceId: serviceId, + final fileToSave = OperationFileModel( + operationId: operationId, name: cleanFileName.fileNameWithoutExtension(), extension: cleanFileName.fileExtension(), storagePath: storagePath, @@ -327,19 +327,19 @@ class ServicesRepository { } final response = await _supabase - .from('service_file') + .from('operation_file') .insert(fileToSave.toMap()) .select() .single(); - return ServiceFileModel.fromMap(response); + return OperationFileModel.fromMap(response); } catch (e) { throw 'Errore durante l\'upload: $e'; } } Future copyFileToCustomer({ - required ServiceFileModel file, + required OperationFileModel file, required String customerId, }) async { CustomerFileModel fileToCopy = CustomerFileModel( @@ -352,14 +352,17 @@ class ServicesRepository { await _customerRepository.saveFileReference(fileToCopy); } - Future deleteServiceFiles(List files) async { + Future deleteOperationFiles(List files) async { if (files.isEmpty) return; // 1. Prepariamo le liste di ID e di Percorsi final List idsToDelete = files.map((f) => f.id!).toList(); final List storagePaths = files.map((f) => f.storagePath).toList(); try { - await _supabase.from('service_file').delete().inFilter('id', idsToDelete); + await _supabase + .from('operation_file') + .delete() + .inFilter('id', idsToDelete); await _supabase.storage.from('documents').remove(storagePaths); diff --git a/lib/features/operations/models/energy_service_model.dart b/lib/features/operations/models/energy_operation_model.dart similarity index 74% rename from lib/features/operations/models/energy_service_model.dart rename to lib/features/operations/models/energy_operation_model.dart index 9cf9b54..817d76e 100644 --- a/lib/features/operations/models/energy_service_model.dart +++ b/lib/features/operations/models/energy_operation_model.dart @@ -2,38 +2,38 @@ import 'package:equatable/equatable.dart'; enum EnergyType { luce, gas } // Mappa il tuo public.energy_type -class EnergyServiceModel extends Equatable { +class EnergyOperationModel extends Equatable { final String? id; final DateTime? createdAt; final EnergyType type; final DateTime expiration; final String providerId; - final String? serviceId; + final String? operationId; - const EnergyServiceModel({ + const EnergyOperationModel({ this.id, this.createdAt, required this.type, required this.expiration, required this.providerId, - this.serviceId, + this.operationId, }); - EnergyServiceModel copyWith({ + EnergyOperationModel copyWith({ String? id, DateTime? createdAt, EnergyType? type, DateTime? expiration, String? providerId, - String? serviceId, + String? operationId, }) { - return EnergyServiceModel( + return EnergyOperationModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, type: type ?? this.type, expiration: expiration ?? this.expiration, providerId: providerId ?? this.providerId, - serviceId: serviceId ?? this.serviceId, + operationId: operationId ?? this.operationId, ); } @@ -44,11 +44,11 @@ class EnergyServiceModel extends Equatable { type, expiration, providerId, - serviceId, + operationId, ]; - factory EnergyServiceModel.fromMap(Map map) { - return EnergyServiceModel( + factory EnergyOperationModel.fromMap(Map map) { + return EnergyOperationModel( id: map['id'], createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) @@ -56,7 +56,7 @@ class EnergyServiceModel extends Equatable { type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce, expiration: DateTime.parse(map['expiration']), providerId: map['provider_id'], - serviceId: map['service_id'], + operationId: map['operation_id'], ); } @@ -66,7 +66,7 @@ class EnergyServiceModel extends Equatable { 'type': type.name, // .name trasforma l'enum in 'luce' o 'gas' 'expiration': expiration.toIso8601String(), 'provider_id': providerId, - 'service_id': serviceId, + 'operation_id': operationId, }; } } diff --git a/lib/features/operations/models/entertainment_service_model.dart b/lib/features/operations/models/entertainment_operation_model.dart similarity index 75% rename from lib/features/operations/models/entertainment_service_model.dart rename to lib/features/operations/models/entertainment_operation_model.dart index f34743a..49930b3 100644 --- a/lib/features/operations/models/entertainment_service_model.dart +++ b/lib/features/operations/models/entertainment_operation_model.dart @@ -1,40 +1,40 @@ import 'package:equatable/equatable.dart'; -class EntertainmentServiceModel extends Equatable { +class EntertainmentOperationModel extends Equatable { final String? id; final DateTime? createdAt; final String type; // es. Sky, DAZN, ecc. final bool constrained; // Vincolato? final DateTime constrainExpiration; - final String? serviceId; + final String? operationId; final String? providerId; - const EntertainmentServiceModel({ + const EntertainmentOperationModel({ this.id, this.createdAt, required this.type, required this.constrained, required this.constrainExpiration, - this.serviceId, + this.operationId, this.providerId, }); - EntertainmentServiceModel copyWith({ + EntertainmentOperationModel copyWith({ String? id, DateTime? createdAt, String? type, bool? constrained, DateTime? constrainExpiration, - String? serviceId, + String? operationId, String? providerId, }) { - return EntertainmentServiceModel( + return EntertainmentOperationModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, type: type ?? this.type, constrained: constrained ?? this.constrained, constrainExpiration: constrainExpiration ?? this.constrainExpiration, - serviceId: serviceId ?? this.serviceId, + operationId: operationId ?? this.operationId, providerId: providerId ?? this.providerId, ); } @@ -46,12 +46,12 @@ class EntertainmentServiceModel extends Equatable { type, constrained, constrainExpiration, - serviceId, + operationId, providerId, ]; - factory EntertainmentServiceModel.fromMap(Map map) { - return EntertainmentServiceModel( + factory EntertainmentOperationModel.fromMap(Map map) { + return EntertainmentOperationModel( id: map['id'], createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) @@ -59,7 +59,7 @@ class EntertainmentServiceModel extends Equatable { type: map['type'], constrained: map['constrained'] ?? false, constrainExpiration: DateTime.parse(map['constrain_expiration']), - serviceId: map['service_id'], + operationId: map['operation_id'], providerId: map['provider_id'], ); } @@ -70,7 +70,7 @@ class EntertainmentServiceModel extends Equatable { 'type': type, 'constrained': constrained, 'constrain_expiration': constrainExpiration.toIso8601String(), - 'service_id': serviceId, + 'operation_id': operationId, 'provider_id': providerId, }; } diff --git a/lib/features/operations/models/fin_service_model.dart b/lib/features/operations/models/fin_operation_model.dart similarity index 69% rename from lib/features/operations/models/fin_service_model.dart rename to lib/features/operations/models/fin_operation_model.dart index 9cdaa5a..d7bf513 100644 --- a/lib/features/operations/models/fin_service_model.dart +++ b/lib/features/operations/models/fin_operation_model.dart @@ -1,51 +1,51 @@ import 'package:equatable/equatable.dart'; -class FinServiceModel extends Equatable { +class FinOperationModel extends Equatable { final String? id; final DateTime? createdAt; final DateTime expiration; - final String? serviceId; + final String? operationId; final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.) final String? providerId; - const FinServiceModel({ + const FinOperationModel({ this.id, this.createdAt, required this.expiration, - this.serviceId, + this.operationId, this.modelId, this.providerId, }); - FinServiceModel copyWith({ + FinOperationModel copyWith({ String? id, DateTime? createdAt, DateTime? expiration, - String? serviceId, + String? operationId, String? modelId, String? providerId, }) { - return FinServiceModel( + return FinOperationModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, expiration: expiration ?? this.expiration, - serviceId: serviceId ?? this.serviceId, + operationId: operationId ?? this.operationId, modelId: modelId ?? this.modelId, providerId: providerId ?? this.providerId, ); } @override - List get props => [id, createdAt, expiration, serviceId, modelId]; + List get props => [id, createdAt, expiration, operationId, modelId]; - factory FinServiceModel.fromMap(Map map) { - return FinServiceModel( + factory FinOperationModel.fromMap(Map map) { + return FinOperationModel( id: map['id'], createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) : null, expiration: DateTime.parse(map['expiration']), - serviceId: map['service_id'], + operationId: map['operation_id'], modelId: map['model_id'], providerId: map['provider_id'], ); @@ -55,7 +55,7 @@ class FinServiceModel extends Equatable { return { if (id != null) 'id': id, 'expiration': expiration.toIso8601String(), - 'service_id': serviceId, + 'operation_id': operationId, 'model_id': modelId, 'provider_id': providerId, }; diff --git a/lib/features/operations/models/service_file_model.dart b/lib/features/operations/models/operation_file_model.dart similarity index 81% rename from lib/features/operations/models/service_file_model.dart rename to lib/features/operations/models/operation_file_model.dart index f804166..376c8c1 100644 --- a/lib/features/operations/models/service_file_model.dart +++ b/lib/features/operations/models/operation_file_model.dart @@ -2,23 +2,23 @@ import 'dart:typed_data'; import 'package:equatable/equatable.dart'; -class ServiceFileModel extends Equatable { +class OperationFileModel extends Equatable { final String? id; final DateTime? createdAt; final String name; final String extension; final String storagePath; - final String serviceId; + final String operationId; final int fileSize; final Uint8List? localBytes; - const ServiceFileModel({ + const OperationFileModel({ this.id, this.createdAt, required this.name, required this.extension, required this.storagePath, - required this.serviceId, + required this.operationId, required this.fileSize, this.localBytes, }); @@ -37,30 +37,30 @@ class ServiceFileModel extends Equatable { bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf'; - ServiceFileModel copyWith({ + OperationFileModel copyWith({ String? id, DateTime? createdAt, String? name, String? extension, String? storagePath, - String? serviceId, + String? operationId, int? fileSize, Uint8List? localBytes, }) { - return ServiceFileModel( + return OperationFileModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, name: name ?? this.name, extension: extension ?? this.extension, storagePath: storagePath ?? this.storagePath, - serviceId: serviceId ?? this.serviceId, + operationId: operationId ?? this.operationId, fileSize: fileSize ?? this.fileSize, localBytes: localBytes ?? this.localBytes, ); } - factory ServiceFileModel.fromMap(Map map) { - return ServiceFileModel( + factory OperationFileModel.fromMap(Map map) { + return OperationFileModel( id: map['id'] as String, createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) @@ -68,7 +68,7 @@ class ServiceFileModel extends Equatable { name: map['name'] ?? '', extension: map['extension'] ?? '', storagePath: map['storage_path'] ?? '', - serviceId: map['service_id']?.toString() ?? '', + operationId: map['operation_id']?.toString() ?? '', fileSize: map['file_size'] is int ? map['file_size'] : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, @@ -81,7 +81,7 @@ class ServiceFileModel extends Equatable { 'name': name, 'extension': extension, 'storage_path': storagePath, - 'service_id': serviceId, + 'operation_id': operationId, 'file_size': fileSize, }; } @@ -93,7 +93,7 @@ class ServiceFileModel extends Equatable { name, extension, storagePath, - serviceId, + operationId, fileSize, localBytes, ]; diff --git a/lib/features/operations/models/service_model.dart b/lib/features/operations/models/operation_model.dart similarity index 67% rename from lib/features/operations/models/service_model.dart rename to lib/features/operations/models/operation_model.dart index 86c0bf4..b938d07 100644 --- a/lib/features/operations/models/service_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -1,11 +1,11 @@ import 'package:equatable/equatable.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/operations/models/energy_service_model.dart'; -import 'package:flux/features/operations/models/entertainment_service_model.dart'; -import 'package:flux/features/operations/models/fin_service_model.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; // <-- Aggiunto Import +import 'package:flux/features/operations/models/energy_operation_model.dart'; +import 'package:flux/features/operations/models/entertainment_operation_model.dart'; +import 'package:flux/features/operations/models/fin_operation_model.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; // <-- Aggiunto Import -class ServiceModel extends Equatable { +class OperationModel extends Equatable { final String? id; final DateTime? createdAt; final String storeId; @@ -26,14 +26,14 @@ class ServiceModel extends Equatable { final int telepass; // Moduli (Liste) - final List energyServices; - final List finServices; - final List entertainmentServices; + final List energyOperations; + final List finOperations; + final List entertainmentOperations; // ALLEGATI (Aggiunto) - final List files; + final List files; - const ServiceModel({ + const OperationModel({ this.id, this.createdAt, required this.storeId, @@ -48,15 +48,15 @@ class ServiceModel extends Equatable { this.nip = 0, this.unica = 0, this.telepass = 0, - this.energyServices = const [], - this.finServices = const [], - this.entertainmentServices = const [], + this.energyOperations = const [], + this.finOperations = const [], + this.entertainmentOperations = const [], this.files = const [], // <-- Aggiunto default vuoto this.customerDisplayName, required this.companyId, }); - ServiceModel copyWith({ + OperationModel copyWith({ String? id, DateTime? createdAt, String? storeId, @@ -71,14 +71,14 @@ class ServiceModel extends Equatable { int? nip, int? unica, int? telepass, - List? energyServices, - List? finServices, - List? entertainmentServices, - List? files, // <-- Aggiunto + List? energyOperations, + List? finOperations, + List? entertainmentOperations, + List? files, // <-- Aggiunto String? customerDisplayName, String? companyId, }) { - return ServiceModel( + return OperationModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, storeId: storeId ?? this.storeId, @@ -93,10 +93,10 @@ class ServiceModel extends Equatable { nip: nip ?? this.nip, unica: unica ?? this.unica, telepass: telepass ?? this.telepass, - energyServices: energyServices ?? this.energyServices, - finServices: finServices ?? this.finServices, - entertainmentServices: - entertainmentServices ?? this.entertainmentServices, + energyOperations: energyOperations ?? this.energyOperations, + finOperations: finOperations ?? this.finOperations, + entertainmentOperations: + entertainmentOperations ?? this.entertainmentOperations, files: files ?? this.files, // <-- Aggiunto customerDisplayName: customerDisplayName ?? this.customerDisplayName, companyId: companyId ?? this.companyId, @@ -119,16 +119,16 @@ class ServiceModel extends Equatable { nip, unica, telepass, - energyServices, - finServices, - entertainmentServices, + energyOperations, + finOperations, + entertainmentOperations, files, // <-- Aggiunto customerDisplayName, companyId, ]; - factory ServiceModel.fromMap(Map map) { - return ServiceModel( + factory OperationModel.fromMap(Map map) { + return OperationModel( id: map['id'].toString(), createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) @@ -147,26 +147,26 @@ class ServiceModel extends Equatable { telepass: map['telepass'] ?? 0, // Estrazione sicura liste collegate - energyServices: - (map['energy_service'] as List?) - ?.map((x) => EnergyServiceModel.fromMap(x)) + energyOperations: + (map['energy_operation'] as List?) + ?.map((x) => EnergyOperationModel.fromMap(x)) .toList() ?? const [], - finServices: - (map['fin_service'] as List?) - ?.map((x) => FinServiceModel.fromMap(x)) + finOperations: + (map['fin_operation'] as List?) + ?.map((x) => FinOperationModel.fromMap(x)) .toList() ?? const [], - entertainmentServices: - (map['entertainment_service'] as List?) - ?.map((x) => EntertainmentServiceModel.fromMap(x)) + entertainmentOperations: + (map['entertainment_operation'] as List?) + ?.map((x) => EntertainmentOperationModel.fromMap(x)) .toList() ?? const [], // I FILE! (Assicurati che la foreign key su Supabase usi esattamente questo nome) files: - (map['service_file'] as List?) - ?.map((x) => ServiceFileModel.fromMap(x)) + (map['operation_file'] as List?) + ?.map((x) => OperationFileModel.fromMap(x)) .toList() ?? const [], diff --git a/lib/features/operations/ui/service_action_card.dart b/lib/features/operations/ui/operation_action_card.dart similarity index 96% rename from lib/features/operations/ui/service_action_card.dart rename to lib/features/operations/ui/operation_action_card.dart index ef06dc7..c54c2e8 100644 --- a/lib/features/operations/ui/service_action_card.dart +++ b/lib/features/operations/ui/operation_action_card.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -class ServiceActionCard extends StatelessWidget { +class OperationActionCard extends StatelessWidget { final String title; final IconData icon; final VoidCallback onTap; final Color color; final int count; - const ServiceActionCard({ + const OperationActionCard({ super.key, required this.title, required this.icon, diff --git a/lib/features/operations/ui/service_form_screen/action_card.dart b/lib/features/operations/ui/operation_form_screen/action_card.dart similarity index 100% rename from lib/features/operations/ui/service_form_screen/action_card.dart rename to lib/features/operations/ui/operation_form_screen/action_card.dart diff --git a/lib/features/operations/ui/service_form_screen/attachment_section.dart b/lib/features/operations/ui/operation_form_screen/attachment_section.dart similarity index 87% rename from lib/features/operations/ui/service_form_screen/attachment_section.dart rename to lib/features/operations/ui/operation_form_screen/attachment_section.dart index 1a5e960..5980313 100644 --- a/lib/features/operations/ui/service_form_screen/attachment_section.dart +++ b/lib/features/operations/ui/operation_form_screen/attachment_section.dart @@ -5,9 +5,9 @@ import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/widgets/image_viewer_widget.dart'; import 'package:flux/core/widgets/pdf_viewer_widget.dart'; import 'package:flux/core/widgets/qr_upload_dialog.dart'; -import 'package:flux/features/operations/blocs/service_files_bloc.dart'; +import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; class AttachmentsSection extends StatelessWidget { const AttachmentsSection({super.key}); @@ -22,27 +22,29 @@ class AttachmentsSection extends StatelessWidget { ); if (result != null && context.mounted) { - context.read().add(AddServiceFilesEvent(result.files)); + context.read().add( + AddOperationFilesEvent(result.files), + ); } } @override Widget build(BuildContext context) { - ServiceFilesBloc serviceFilesBloc = BlocProvider.of( + OperationFilesBloc operationFilesBloc = BlocProvider.of( context, ); - return BlocListener( + return BlocListener( listenWhen: (previous, current) => - previous.currentService?.id == null && - current.currentService?.id != null, + previous.currentOperation?.id == null && + current.currentOperation?.id != null, listener: (context, state) { // FIGASSA! La pratica è stata salvata e ora ha un ID. // Diciamo al Bloc dei file di agganciarsi al database. - final newId = state.currentService!.id!; - context.read().add(ServiceSavedEvent(newId)); + final newId = state.currentOperation!.id!; + context.read().add(OperationsavedEvent(newId)); }, - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -125,8 +127,8 @@ class AttachmentsSection extends StatelessWidget { final isSelected = state.selectedFiles.contains(file); return GestureDetector( - onTap: () => serviceFilesBloc.add( - ToggleServiceFileSelectionEvent(file), + onTap: () => operationFilesBloc.add( + ToggleOperationFileSelectionEvent(file), ), onDoubleTap: () => _handleDoubleClick(context, file), child: Card( @@ -216,7 +218,7 @@ class AttachmentsSection extends StatelessWidget { label: const Text("Elimina"), onPressed: () { // Qui lancerai l'evento per eliminare i file selezionati! - // Es: serviceFilesBloc.add(DeleteSelectedFilesEvent()); + // Es: operationFilesBloc.add(DeleteSelectedFilesEvent()); }, ), const SizedBox(width: 8), @@ -243,14 +245,14 @@ class AttachmentsSection extends StatelessWidget { } Future _handleGenerateQr(BuildContext context) async { - final cubit = context.read(); - var currentService = cubit.state.currentService; + final cubit = context.read(); + var currentOperation = cubit.state.currentOperation; // 1. CATTURIAMO IL BLOC MENTRE SIAMO ANCORA NELLA PAGINA - final serviceFilesBloc = context.read(); + final operationFilesBloc = context.read(); // 2. SE LA PRATICA E' NUOVA (Manca l'ID) - if (currentService == null || currentService.id == null) { + if (currentOperation == null || currentOperation.id == null) { // NIENTE BlocListener qui! Solo un semplice Dialog di conferma final bool? confirm = await showDialog( context: context, @@ -275,42 +277,42 @@ class AttachmentsSection extends StatelessWidget { if (confirm != true) return; // Utente ha annullato // Salviamo forzatamente in bozza - await cubit.saveCurrentService( + await cubit.saveCurrentOperation( isBozza: true, shouldPop: false, - files: serviceFilesBloc.state.localFiles, + files: operationFilesBloc.state.localFiles, ); // Recuperiamo il servizio aggiornato con l'ID! - currentService = cubit.state.currentService; + currentOperation = cubit.state.currentOperation; - if (currentService?.id == null) return; + if (currentOperation?.id == null) return; } // 3. MOSTRIAMO IL QR CODE (Con il Ponte e l'Auto-Chiusura!) if (context.mounted) { - final nomePratica = "Pratica ${currentService?.customerDisplayName ?? ''}" - .trim(); + final nomePratica = + "Pratica ${currentOperation?.customerDisplayName ?? ''}".trim(); showDialog( context: context, builder: (dialogContext) => BlocProvider.value( // INIETTIAMO IL BLOC NEL CONTESTO DEL DIALOG ALIENO - value: serviceFilesBloc, + value: operationFilesBloc, // ORA METTIAMO L'AUTO-CHIUSURA SUL QR CODE! - child: BlocListener( + child: BlocListener( listener: (context, state) { // Se arrivano file remoti e lo stato è success, chiudiamo il QR! // (Nota: usiamo dialogContext per assicurarci di chiudere il popup giusto) - if (state.status == ServiceFilesStatus.success && + if (state.status == OperationFilesStatus.success && state.remoteFiles.isNotEmpty) { Navigator.of(dialogContext).pop(); } }, child: QrUploadDialog( deepLinkUrl: - 'fluxapp:///operation/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', + 'fluxapp:///operation/${currentOperation!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', title: 'Scatta per\n$nomePratica', ), ), @@ -322,7 +324,7 @@ class AttachmentsSection extends StatelessWidget { // --- LOGICA DI COPIA AL CLIENTE --- void saveAndCopyFilesToCustomer( BuildContext context, - List files, + List files, ) { showDialog( context: context, @@ -341,7 +343,7 @@ class AttachmentsSection extends StatelessWidget { onPressed: () { Navigator.pop(ctx); // 1. Diciamo al Cubit di salvare in Bozza e fare la copia - context.read().saveAndCopyFileToCustomer(files); + context.read().saveAndCopyFileToCustomer(files); }, child: const Text("Salva e Copia"), ), @@ -351,7 +353,7 @@ class AttachmentsSection extends StatelessWidget { } // --- LOGICA DI VISUALIZZAZIONE OVERLAY --- - void _handleDoubleClick(BuildContext context, ServiceFileModel file) { + void _handleDoubleClick(BuildContext context, OperationFileModel file) { showDialog( context: context, barrierDismissible: true, diff --git a/lib/features/operations/ui/service_form_screen/customer_section.dart b/lib/features/operations/ui/operation_form_screen/customer_section.dart similarity index 96% rename from lib/features/operations/ui/service_form_screen/customer_section.dart rename to lib/features/operations/ui/operation_form_screen/customer_section.dart index bb0a7e8..b3546db 100644 --- a/lib/features/operations/ui/service_form_screen/customer_section.dart +++ b/lib/features/operations/ui/operation_form_screen/customer_section.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flux/features/customers/ui/customer_search_sheet.dart'; -import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; class CustomerSection extends StatelessWidget { - final ServiceModel operation; + final OperationModel operation; const CustomerSection({super.key, required this.operation}); diff --git a/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart b/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart similarity index 94% rename from lib/features/operations/ui/service_form_screen/energy_service_dialog.dart rename to lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart index 04c0ef4..f44dd40 100644 --- a/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart +++ b/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart @@ -2,32 +2,32 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; -import 'package:flux/features/operations/models/energy_service_model.dart'; // Assicurati degli import +import 'package:flux/features/operations/models/energy_operation_model.dart'; // Assicurati degli import -class EnergyServiceDialog extends StatefulWidget { - final List initialServices; +class EnergyOperationDialog extends StatefulWidget { + final List initialOperations; final String currentStoreId; // Ci serve per sapere per quale negozio caricare i gestori - const EnergyServiceDialog({ + const EnergyOperationDialog({ super.key, - required this.initialServices, + required this.initialOperations, required this.currentStoreId, }); @override - State createState() => _EnergyServiceDialogState(); + State createState() => _EnergyOperationDialogState(); } -class _EnergyServiceDialogState extends State { +class _EnergyOperationDialogState extends State { // Lista temporanea per non "sporcare" il cubit finché non si preme Conferma - late List _tempList; + late List _tempList; bool _isAddingNew = false; @override void initState() { super.initState(); - _tempList = List.from(widget.initialServices); + _tempList = List.from(widget.initialOperations); // Al caricamento della modale, chiediamo al Cubit di recuperare i gestori veri! context.read().loadActiveProvidersForStore( widget.currentStoreId, @@ -52,9 +52,9 @@ class _EnergyServiceDialogState extends State { // Cambia vista in base al flag child: _isAddingNew ? _EnergyForm( - onSave: (newService) { + onSave: (newOperation) { setState(() { - _tempList.add(newService); + _tempList.add(newOperation); _isAddingNew = false; // Torna alla lista }); }, @@ -101,7 +101,7 @@ class _EnergyServiceDialogState extends State { // VISTA 1: LA LISTA DEI CONTRATTI // ========================================== class _EnergyList extends StatelessWidget { - final List operations; + final List operations; final List activeProviders; // <--- NUOVO: La lista vera dal Cubit final Function(int) onDelete; @@ -193,7 +193,7 @@ class _EnergyList extends StatelessWidget { // VISTA 2: IL FORM DI INSERIMENTO // ========================================== class _EnergyForm extends StatefulWidget { - final Function(EnergyServiceModel) onSave; + final Function(EnergyOperationModel) onSave; final VoidCallback onCancel; const _EnergyForm({required this.onSave, required this.onCancel}); @@ -400,12 +400,12 @@ class _EnergyFormState extends State<_EnergyForm> { (_selectedProviderId == null || _selectedExpiration == null) ? null // Disabilitato se mancano dati obbligatori : () { - final newService = EnergyServiceModel( + final newOperation = EnergyOperationModel( type: _selectedType, expiration: _selectedExpiration!, providerId: _selectedProviderId!, ); - widget.onSave(newService); + widget.onSave(newOperation); }, child: const Text("Salva Contratto"), ), diff --git a/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart b/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart similarity index 92% rename from lib/features/operations/ui/service_form_screen/entertainment_service_card.dart rename to lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart index d7d08a5..88fbc27 100644 --- a/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart +++ b/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart @@ -3,34 +3,34 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; -import 'package:flux/features/operations/data/services_repository.dart'; -import 'package:flux/features/operations/models/entertainment_service_model.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/operations/models/entertainment_operation_model.dart'; import 'package:get_it/get_it.dart'; -class EntertainmentServiceDialog extends StatefulWidget { - final List initialServices; +class EntertainmentOperationDialog extends StatefulWidget { + final List initialOperations; final String currentStoreId; - const EntertainmentServiceDialog({ + const EntertainmentOperationDialog({ super.key, - required this.initialServices, + required this.initialOperations, required this.currentStoreId, }); @override - State createState() => - _EntertainmentServiceDialogState(); + State createState() => + _EntertainmentOperationDialogState(); } -class _EntertainmentServiceDialogState - extends State { - late List _tempList; +class _EntertainmentOperationDialogState + extends State { + late List _tempList; bool _isAddingNew = false; @override void initState() { super.initState(); - _tempList = List.from(widget.initialServices); + _tempList = List.from(widget.initialOperations); // Carichiamo i provider attivi per lo store corrente context.read().loadActiveProvidersForStore( widget.currentStoreId, @@ -57,8 +57,8 @@ class _EntertainmentServiceDialogState child: _isAddingNew ? _EntertainmentForm( // Il form che abbiamo creato prima - onSave: (newService) => setState(() { - _tempList.add(newService); + onSave: (newOperation) => setState(() { + _tempList.add(newOperation); _isAddingNew = false; }), onCancel: () => setState(() => _isAddingNew = false), @@ -94,7 +94,7 @@ class _EntertainmentServiceDialogState } class _EntertainmentList extends StatelessWidget { - final List operations; + final List operations; final List allProviders; final Function(int) onDelete; final VoidCallback onAddTap; @@ -194,7 +194,7 @@ class _EntertainmentList extends StatelessWidget { // ---ENTERTAINMENT FORM (MODALE)--- class _EntertainmentForm extends StatefulWidget { - final Function(EntertainmentServiceModel) onSave; + final Function(EntertainmentOperationModel) onSave; final VoidCallback onCancel; const _EntertainmentForm({required this.onSave, required this.onCancel}); @@ -280,7 +280,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> { const SizedBox(height: 8), // Suggerimenti rapidi (Chip) FutureBuilder>( - future: GetIt.I().fetchTopEntertainmentTypes( + future: GetIt.I().fetchTopEntertainmentTypes( GetIt.I().state.company!.id!, ), builder: (context, snapshot) { @@ -376,7 +376,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> { (_selectedProviderId == null || _typeController.text.isEmpty) ? null : () => widget.onSave( - EntertainmentServiceModel( + EntertainmentOperationModel( providerId: _selectedProviderId!, type: _typeController.text, constrained: _isConstrained, diff --git a/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart b/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart similarity index 96% rename from lib/features/operations/ui/service_form_screen/finance_service_dialog.dart rename to lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart index a8196c9..fa838a9 100644 --- a/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart +++ b/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart @@ -5,36 +5,36 @@ 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/model_model.dart'; import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; -import 'package:flux/features/operations/models/fin_service_model.dart'; +import 'package:flux/features/operations/models/fin_operation_model.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; // =========================================================================== // DIALOG PRINCIPALE // =========================================================================== -class FinanceServiceDialog extends StatefulWidget { - final List initialServices; +class FinanceOperationDialog extends StatefulWidget { + final List initialOperations; final String currentStoreId; final ProductCubit productCubit; - const FinanceServiceDialog({ + const FinanceOperationDialog({ super.key, - required this.initialServices, + required this.initialOperations, required this.currentStoreId, required this.productCubit, }); @override - State createState() => _FinanceServiceDialogState(); + State createState() => _FinanceOperationDialogState(); } -class _FinanceServiceDialogState extends State { - late List _tempList; +class _FinanceOperationDialogState extends State { + late List _tempList; bool _isAddingNew = false; @override void initState() { super.initState(); - _tempList = List.from(widget.initialServices); + _tempList = List.from(widget.initialOperations); // Carichiamo i dati necessari dai Cubit context.read().loadActiveProvidersForStore( widget.currentStoreId, @@ -109,7 +109,7 @@ class _FinanceServiceDialogState extends State { // VISTA LISTA (STORICA) // =========================================================================== class _FinanceList extends StatelessWidget { - final List operations; + final List operations; final List allProviders; final List allModels; final Function(int) onDelete; @@ -221,7 +221,7 @@ class _FinanceList extends StatelessWidget { // FORM CON OMNI-SEARCH // =========================================================================== class _FinanceForm extends StatefulWidget { - final Function(FinServiceModel) onSave; + final Function(FinOperationModel) onSave; final VoidCallback onCancel; const _FinanceForm({required this.onSave, required this.onCancel}); @@ -428,7 +428,7 @@ class _FinanceFormState extends State<_FinanceForm> { : () { final now = DateTime.now(); widget.onSave( - FinServiceModel( + FinOperationModel( providerId: _selectedProviderId!, modelId: _selectedModel!.id!, expiration: DateTime( diff --git a/lib/features/operations/ui/service_form_screen/general_info_section.dart b/lib/features/operations/ui/operation_form_screen/general_info_section.dart similarity index 88% rename from lib/features/operations/ui/service_form_screen/general_info_section.dart rename to lib/features/operations/ui/operation_form_screen/general_info_section.dart index f679b9f..ceaf3db 100644 --- a/lib/features/operations/ui/service_form_screen/general_info_section.dart +++ b/lib/features/operations/ui/operation_form_screen/general_info_section.dart @@ -1,10 +1,10 @@ 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/models/service_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; class GeneralInfoSection extends StatelessWidget { - final ServiceModel operation; + final OperationModel operation; const GeneralInfoSection({super.key, required this.operation}); @override @@ -44,7 +44,7 @@ class GeneralInfoSection extends StatelessWidget { prefixIcon: Icon(Icons.phone), ), onChanged: (val) { - context.read().updateField(number: val); + context.read().updateField(number: val); }, ), const SizedBox(height: 16), @@ -63,7 +63,7 @@ class GeneralInfoSection extends StatelessWidget { activeThumbColor: Colors.orange, contentPadding: EdgeInsets.zero, onChanged: (val) { - context.read().updateField(isBozza: val); + context.read().updateField(isBozza: val); }, ), ), @@ -79,7 +79,9 @@ class GeneralInfoSection extends StatelessWidget { activeThumbColor: Colors.green, contentPadding: EdgeInsets.zero, onChanged: (val) { - context.read().updateField(resultOk: val); + context.read().updateField( + resultOk: val, + ); }, ), ), @@ -100,7 +102,7 @@ class GeneralInfoSection extends StatelessWidget { alignLabelWithHint: true, ), onChanged: (val) { - context.read().updateField(note: val); + context.read().updateField(note: val); }, ), ], diff --git a/lib/features/operations/ui/service_form_screen/int_dialogs.dart b/lib/features/operations/ui/operation_form_screen/int_dialogs.dart similarity index 100% rename from lib/features/operations/ui/service_form_screen/int_dialogs.dart rename to lib/features/operations/ui/operation_form_screen/int_dialogs.dart diff --git a/lib/features/operations/ui/service_form_screen/service_form_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart similarity index 76% rename from lib/features/operations/ui/service_form_screen/service_form_screen.dart rename to lib/features/operations/ui/operation_form_screen/operation_form_screen.dart index 400dc41..4608a9a 100644 --- a/lib/features/operations/ui/service_form_screen/service_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart @@ -1,49 +1,49 @@ 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/models/service_model.dart'; -import 'package:flux/features/operations/ui/service_form_screen/attachment_section.dart'; -import 'package:flux/features/operations/ui/service_form_screen/customer_section.dart'; -import 'package:flux/features/operations/ui/service_form_screen/general_info_section.dart'; -import 'package:flux/features/operations/ui/service_form_screen/services_grid.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/attachment_section.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/customer_section.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/general_info_section.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/operations_grid.dart'; -class ServiceFormScreen extends StatefulWidget { - final String? serviceId; - final ServiceModel? existingService; // <-- AGGIUNTO +class OperationFormScreen extends StatefulWidget { + final String? operationId; + final OperationModel? existingOperation; // <-- AGGIUNTO - const ServiceFormScreen({ + const OperationFormScreen({ super.key, - this.serviceId, - this.existingService, // <-- AGGIUNTO + this.operationId, + this.existingOperation, // <-- AGGIUNTO }); @override - State createState() => _ServiceFormScreenState(); + State createState() => _OperationFormScreenState(); } -class _ServiceFormScreenState extends State { +class _OperationFormScreenState 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, + context.read().initOperationForm( + existingOperation: widget.existingOperation, + operationId: widget.operationId, ); }); } void _performSave(BuildContext context, {required bool isBozza}) { FocusScope.of(context).unfocus(); - context.read().saveCurrentService(isBozza: isBozza); + context.read().saveCurrentOperation(isBozza: isBozza); } @override Widget build(BuildContext context) { - return BlocConsumer( + return BlocConsumer( listener: (context, state) { - if (state.status == ServicesStatus.saved) { + if (state.status == OperationsStatus.saved) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Pratica salvata con successo!"), @@ -52,7 +52,7 @@ class _ServiceFormScreenState extends State { ); Navigator.pop(context); } - if (state.status == ServicesStatus.failure) { + if (state.status == OperationsStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text("Errore: ${state.errorMessage ?? ''}"), @@ -60,7 +60,7 @@ class _ServiceFormScreenState extends State { ), ); } - if (state.status == ServicesStatus.savedNoPop) { + if (state.status == OperationsStatus.savedNoPop) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Pratica salvata con successo!"), @@ -70,9 +70,9 @@ class _ServiceFormScreenState extends State { } }, builder: (context, state) { - final operation = state.currentService; - final isSaving = state.status == ServicesStatus.saving; - final isEditMode = widget.serviceId != null; + final operation = state.currentOperation; + final isSaving = state.status == OperationsStatus.saving; + final isEditMode = widget.operationId != null; return Scaffold( appBar: AppBar( @@ -120,7 +120,7 @@ class _ServiceFormScreenState extends State { GeneralInfoSection(operation: operation), const SizedBox(height: 24), - ServicesGrid(operation: operation), + OperationsGrid(operation: operation), const SizedBox(height: 32), AttachmentsSection(), diff --git a/lib/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart similarity index 92% rename from lib/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart rename to lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart index bd64cf5..8adb21c 100644 --- a/lib/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart +++ b/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart @@ -3,24 +3,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:flux/features/operations/blocs/service_files_bloc.dart'; +import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; -class ServiceMobileUploadScreen extends StatefulWidget { - final String serviceId; - final String serviceName; +class OperationMobileUploadScreen extends StatefulWidget { + final String operationId; + final String operationName; - const ServiceMobileUploadScreen({ + const OperationMobileUploadScreen({ super.key, - required this.serviceId, - required this.serviceName, + required this.operationId, + required this.operationName, }); @override - State createState() => - _ServiceMobileUploadScreenState(); + State createState() => + _OperationMobileUploadScreenState(); } -class _ServiceMobileUploadScreenState extends State { +class _OperationMobileUploadScreenState + extends State { // 1. LA NOSTRA STAGING AREA (Il "Carrello") final List _stagedFiles = []; @@ -35,10 +36,10 @@ class _ServiceMobileUploadScreenState extends State { @override Widget build(BuildContext context) { - return BlocListener( + return BlocListener( listener: (context, state) { // Quando il BLoC ci dice che ha finito l'upload (Success), chiudiamo la pagina! - if (state.status == ServiceFilesStatus.success && _isUploading) { + if (state.status == OperationFilesStatus.success && _isUploading) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Tutti i file caricati con successo! ✅"), @@ -46,7 +47,7 @@ class _ServiceMobileUploadScreenState extends State { ); Navigator.of(context).pop(); } - if (state.status == ServiceFilesStatus.failure) { + if (state.status == OperationFilesStatus.failure) { setState(() => _isUploading = false); ScaffoldMessenger.of( context, @@ -55,7 +56,7 @@ class _ServiceMobileUploadScreenState extends State { }, child: Scaffold( appBar: AppBar( - title: Text("Upload Pratica:\n${widget.serviceName}"), + title: Text("Upload Pratica:\n${widget.operationName}"), automaticallyImplyLeading: !_isUploading, ), body: Stack( @@ -294,8 +295,8 @@ class _ServiceMobileUploadScreenState extends State { // Diciamo al BLoC di caricare tutti i file. // Usiamo il tuo evento esistente per ogni file (il BLoC li metterà in coda) - final bloc = context.read(); - bloc.add(UploadMultipleServiceFilesEvent(_stagedFiles)); + final bloc = context.read(); + bloc.add(UploadMultipleOperationFilesEvent(_stagedFiles)); // N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"! } diff --git a/lib/features/operations/ui/service_form_screen/services_grid.dart b/lib/features/operations/ui/operation_form_screen/operations_grid.dart similarity index 63% rename from lib/features/operations/ui/service_form_screen/services_grid.dart rename to lib/features/operations/ui/operation_form_screen/operations_grid.dart index bc0ab95..e5e252e 100644 --- a/lib/features/operations/ui/service_form_screen/services_grid.dart +++ b/lib/features/operations/ui/operation_form_screen/operations_grid.dart @@ -2,20 +2,20 @@ 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/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/energy_service_model.dart'; -import 'package:flux/features/operations/models/entertainment_service_model.dart'; -import 'package:flux/features/operations/models/fin_service_model.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:flux/features/operations/ui/service_form_screen/action_card.dart'; -import 'package:flux/features/operations/ui/service_form_screen/energy_service_dialog.dart'; -import 'package:flux/features/operations/ui/service_form_screen/entertainment_service_card.dart'; -import 'package:flux/features/operations/ui/service_form_screen/finance_service_dialog.dart'; -import 'package:flux/features/operations/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello +import 'package:flux/features/operations/models/energy_operation_model.dart'; +import 'package:flux/features/operations/models/entertainment_operation_model.dart'; +import 'package:flux/features/operations/models/fin_operation_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/action_card.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/energy_operation_dialog.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/entertainment_operation_card.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/finance_operation_dialog.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/int_dialogs.dart'; // Assicurati di importare il modello -class ServicesGrid extends StatelessWidget { - final ServiceModel operation; +class OperationsGrid extends StatelessWidget { + final OperationModel operation; - const ServicesGrid({super.key, required this.operation}); + const OperationsGrid({super.key, required this.operation}); @override Widget build(BuildContext context) { @@ -60,7 +60,7 @@ class ServicesGrid extends StatelessWidget { "AL", operation.al, (val) => - context.read().updateField(al: val), + context.read().updateField(al: val), ), ), ActionCard( @@ -73,7 +73,7 @@ class ServicesGrid extends StatelessWidget { "MNP", operation.mnp, (val) => - context.read().updateField(mnp: val), + context.read().updateField(mnp: val), ), ), ActionCard( @@ -86,7 +86,7 @@ class ServicesGrid extends StatelessWidget { "NIP", operation.nip, (val) => - context.read().updateField(nip: val), + context.read().updateField(nip: val), ), ), ActionCard( @@ -98,8 +98,9 @@ class ServicesGrid extends StatelessWidget { context, "Unica", operation.unica, - (val) => - context.read().updateField(unica: val), + (val) => context.read().updateField( + unica: val, + ), ), ), ActionCard( @@ -111,7 +112,7 @@ class ServicesGrid extends StatelessWidget { context, "Telepass", operation.telepass, - (val) => context.read().updateField( + (val) => context.read().updateField( telepass: val, ), ), @@ -120,23 +121,24 @@ class ServicesGrid extends StatelessWidget { // --- MODULI COMPLESSI (Le liste) --- ActionCard( label: "Energia", - count: operation.energyServices.length, + count: operation.energyOperations.length, icon: Icons.bolt, color: Colors.green, onTap: () async { // Apriamo la modale e aspettiamo il risultato - final result = await showDialog>( - context: context, - builder: (context) => EnergyServiceDialog( - currentStoreId: operation.storeId, - initialServices: operation - .energyServices, // Passiamo la lista attuale - ), - ); + final result = + await showDialog>( + context: context, + builder: (context) => EnergyOperationDialog( + currentStoreId: operation.storeId, + initialOperations: operation + .energyOperations, // Passiamo la lista attuale + ), + ); // Se l'utente ha premuto "Conferma" e non "Annulla" o tap fuori if (result != null && context.mounted) { - context.read().updateEnergyServices( + context.read().updateEnergyOperations( result, ); } @@ -144,44 +146,47 @@ class ServicesGrid extends StatelessWidget { ), ActionCard( label: "Finanziam.", - count: operation.finServices.length, + count: operation.finOperations.length, icon: Icons.euro_symbol, color: Colors.teal, onTap: () async { - final result = await showDialog>( + final result = await showDialog>( context: context, - builder: (context) => FinanceServiceDialog( + builder: (context) => FinanceOperationDialog( productCubit: context.read(), currentStoreId: operation.storeId, - initialServices: operation - .finServices, // Passiamo la lista attuale + initialOperations: operation + .finOperations, // Passiamo la lista attuale ), ); if (result != null && context.mounted) { - context.read().updateFinServices(result); + context.read().updateFinOperations( + result, + ); } }, ), ActionCard( label: "Intratten.", - count: operation.entertainmentServices.length, + count: operation.entertainmentOperations.length, icon: Icons.movie_filter_outlined, color: Colors.purple, onTap: () async { final result = - await showDialog>( + await showDialog>( context: context, - builder: (context) => EntertainmentServiceDialog( - initialServices: operation.entertainmentServices, + builder: (context) => EntertainmentOperationDialog( + initialOperations: + operation.entertainmentOperations, currentStoreId: operation.storeId, ), ); if (result != null && context.mounted) { context - .read() - .updateEntertainmentServices(result); + .read() + .updateEntertainmentOperations(result); } }, ), diff --git a/lib/features/operations/ui/services_screen.dart b/lib/features/operations/ui/operations_screen.dart similarity index 77% rename from lib/features/operations/ui/services_screen.dart rename to lib/features/operations/ui/operations_screen.dart index 4935529..a56e73e 100644 --- a/lib/features/operations/ui/services_screen.dart +++ b/lib/features/operations/ui/operations_screen.dart @@ -1,19 +1,19 @@ 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/models/service_model.dart'; -import 'package:flux/features/operations/utils/service_actions.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:flux/features/operations/utils/operation_actions.dart'; import 'package:go_router/go_router.dart'; // Importa i tuoi modelli e cubit -class ServicesScreen extends StatefulWidget { - const ServicesScreen({super.key}); +class OperationsScreen extends StatefulWidget { + const OperationsScreen({super.key}); @override - State createState() => _ServicesScreenState(); + State createState() => _OperationsScreenState(); } -class _ServicesScreenState extends State { +class _OperationsScreenState extends State { final ScrollController _scrollController = ScrollController(); @override @@ -22,12 +22,12 @@ class _ServicesScreenState extends State { // Agganciamo il listener per la paginazione (Scroll Infinito) _scrollController.addListener(_onScroll); // Carichiamo i servizi iniziali - context.read().loadServices(); + context.read().loadOperations(); } void _onScroll() { if (_isBottom) { - context.read().loadServices(); + context.read().loadOperations(); } } @@ -60,16 +60,16 @@ class _ServicesScreenState extends State { ), ], ), - body: BlocBuilder( + body: BlocBuilder( builder: (context, state) { // 1. Stato di caricamento iniziale - if (state.status == ServicesStatus.loading && - state.allServices.isEmpty) { + if (state.status == OperationsStatus.loading && + state.allOperations.isEmpty) { return const Center(child: CircularProgressIndicator()); } // 2. Lista vuota - if (state.allServices.isEmpty) { + if (state.allOperations.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -77,9 +77,9 @@ class _ServicesScreenState extends State { const Text("Nessuna pratica trovata."), const SizedBox(height: 10), ElevatedButton( - onPressed: () => context.read().loadServices( - refresh: true, - ), + onPressed: () => context + .read() + .loadOperations(refresh: true), child: const Text("Riprova"), ), ], @@ -90,15 +90,15 @@ class _ServicesScreenState extends State { // 3. La Lista (con Pull-to-refresh) return RefreshIndicator( onRefresh: () => - context.read().loadServices(refresh: true), + context.read().loadOperations(refresh: true), child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB itemCount: state.hasReachedMax - ? state.allServices.length - : state.allServices.length + 1, + ? state.allOperations.length + : state.allOperations.length + 1, itemBuilder: (context, index) { - if (index >= state.allServices.length) { + if (index >= state.allOperations.length) { return const Center( child: Padding( padding: EdgeInsets.all(16.0), @@ -107,21 +107,21 @@ class _ServicesScreenState extends State { ); } - final operation = state.allServices[index]; - return _buildServiceCard(context, operation); + final operation = state.allOperations[index]; + return _buildOperationCard(context, operation); }, ), ); }, ), floatingActionButton: FloatingActionButton( - onPressed: () => startNewService(context), + onPressed: () => startNewOperation(context), child: const Icon(Icons.add), ), ); } - Widget _buildServiceCard(BuildContext context, ServiceModel operation) { + Widget _buildOperationCard(BuildContext context, OperationModel operation) { return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), elevation: 2, @@ -164,11 +164,11 @@ class _ServicesScreenState extends State { children: [ if (operation.al > 0 || operation.mnp > 0) _miniBadge("📞 Tel", Colors.blue), - if (operation.energyServices.isNotEmpty) + if (operation.energyOperations.isNotEmpty) _miniBadge("⚡ Energy", Colors.green), - if (operation.finServices.isNotEmpty) + if (operation.finOperations.isNotEmpty) _miniBadge("💰 Fin", Colors.purple), - if (operation.entertainmentServices.isNotEmpty) + if (operation.entertainmentOperations.isNotEmpty) _miniBadge("📺 Ent", Colors.red), ], ), @@ -180,7 +180,7 @@ class _ServicesScreenState extends State { extra: operation, // <-- LA MAGIA È QUI: Passa l'oggetto intero! // Teniamo anche il parametro URL per coerenza di routing queryParameters: operation.id != null - ? {'serviceId': operation.id!} + ? {'operationId': operation.id!} : {}, ), ), diff --git a/lib/features/operations/utils/service_actions.dart b/lib/features/operations/utils/operation_actions.dart similarity index 92% rename from lib/features/operations/utils/service_actions.dart rename to lib/features/operations/utils/operation_actions.dart index c0bdf38..35396ae 100644 --- a/lib/features/operations/utils/service_actions.dart +++ b/lib/features/operations/utils/operation_actions.dart @@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; import 'package:go_router/go_router.dart'; /// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore. -void startNewService(BuildContext context) { +void startNewOperation(BuildContext context) { final session = context.read().state; final currentStoreId = session.currentStore?.id; @@ -53,8 +53,8 @@ void startNewService(BuildContext context) { title: Text(member.name), onTap: () { // 1. Inizializza il form nel Cubit - context.read().initServiceForm( - existingService: ServiceModel( + context.read().initOperationForm( + existingOperation: OperationModel( storeId: currentStoreId, employeeId: member.id, number: '', diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a0fe5df..2a57a54 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,13 +1,13 @@ { "@@locale": "en", "welcomeBack": "Welcome back, {name}! 👋", - "latestServices": "Latest Operations", + "latestOperations": "Latest Operations", "masterData": "Master Data", "settings": "Settings", - "newService": "Operation", + "newOperation": "Operation", "expiring_contracts": "Expiring Contracts", "sticky_notes": "Sticky Notes", "my_tasks": "My Tasks", - "latest_service_tickets": "Latest operation tickets" + "latest_operation_tickets": "Latest operation tickets" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 24055c9..a211adf 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -50,15 +50,15 @@ "commonNewPassword": "Nuova Password", "commonNote": "Nota", "commonSave": "Salva", - "commonService": "Servizio", + "commonOperation": "Servizio", "commonSettings": "Impostazioni", "commonStickyNotes": "Sticky Notes", "commonTask": "Attività", "homeExpiringContracts": "Contratti in scadenza", - "homeLatestServiceTickets": "Ultime assistenze", - "homeLatestServices": "Ultimi Servizi", + "homeLatestOperationTickets": "Ultime assistenze", + "homeLatestOperations": "Ultimi Servizi", "homeMyTasks": "Mie Attività", - "homeNewServiceTicket": "Nuova assistenza", + "homeNewOperationTicket": "Nuova assistenza", "homeNoStoreFound": "Nessun negozio trovato", "homeWelcomeBack": "Bentornato, {name}! 👋", "imageViewerWidgetErrorOpening": "Errore durante l'apertura dell'immagine", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c2b13f8..1a5f96f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -220,11 +220,11 @@ abstract class AppLocalizations { /// **'Salva'** String get commonSave; - /// No description provided for @commonService. + /// No description provided for @commonOperation. /// /// In it, this message translates to: /// **'Servizio'** - String get commonService; + String get commonOperation; /// No description provided for @commonSettings. /// @@ -250,17 +250,17 @@ abstract class AppLocalizations { /// **'Contratti in scadenza'** String get homeExpiringContracts; - /// No description provided for @homeLatestServiceTickets. + /// No description provided for @homeLatestOperationTickets. /// /// In it, this message translates to: /// **'Ultime assistenze'** - String get homeLatestServiceTickets; + String get homeLatestOperationTickets; - /// No description provided for @homeLatestServices. + /// No description provided for @homeLatestOperations. /// /// In it, this message translates to: /// **'Ultimi Servizi'** - String get homeLatestServices; + String get homeLatestOperations; /// No description provided for @homeMyTasks. /// @@ -268,11 +268,11 @@ abstract class AppLocalizations { /// **'Mie Attività'** String get homeMyTasks; - /// No description provided for @homeNewServiceTicket. + /// No description provided for @homeNewOperationTicket. /// /// In it, this message translates to: /// **'Nuova assistenza'** - String get homeNewServiceTicket; + String get homeNewOperationTicket; /// No description provided for @homeNoStoreFound. /// diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index eeebff4..7630adc 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -12,10 +12,10 @@ class AppLocalizationsEn extends AppLocalizations { String get homeExpiringContracts => 'Contratti in scadenza'; @override - String get homeLatestServiceTickets => 'Ultime assistenze'; + String get homeLatestOperationTickets => 'Ultime assistenze'; @override - String get homeLatestServices => 'Ultimi Servizi'; + String get homeLatestOperations => 'Ultimi Servizi'; @override String get homeMasterData => 'Anagrafiche'; @@ -24,7 +24,7 @@ class AppLocalizationsEn extends AppLocalizations { String get homeMyTasks => 'Mie Attività'; @override - String get homeNewService => 'Servizio'; + String get homeNewOperation => 'Servizio'; @override String get homeSettings => 'Impostazioni'; @@ -41,7 +41,7 @@ class AppLocalizationsEn extends AppLocalizations { String get homeNoStoreFound => 'Nessun negozio trovato'; @override - String get homeNewServiceTicket => 'Nuova assistenza'; + String get homeNewOperationTicket => 'Nuova assistenza'; @override String get homeNewNote => 'Nota'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 5ca322c..c226972 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -81,7 +81,7 @@ class AppLocalizationsIt extends AppLocalizations { String get commonSave => 'Salva'; @override - String get commonService => 'Servizio'; + String get commonOperation => 'Servizio'; @override String get commonSettings => 'Impostazioni'; @@ -96,16 +96,16 @@ class AppLocalizationsIt extends AppLocalizations { String get homeExpiringContracts => 'Contratti in scadenza'; @override - String get homeLatestServiceTickets => 'Ultime assistenze'; + String get homeLatestOperationTickets => 'Ultime assistenze'; @override - String get homeLatestServices => 'Ultimi Servizi'; + String get homeLatestOperations => 'Ultimi Servizi'; @override String get homeMyTasks => 'Mie Attività'; @override - String get homeNewServiceTicket => 'Nuova assistenza'; + String get homeNewOperationTicket => 'Nuova assistenza'; @override String get homeNoStoreFound => 'Nessun negozio trovato'; diff --git a/lib/main.dart b/lib/main.dart index a6515df..979c1ec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; -import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -27,7 +27,6 @@ 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/operations/data/services_repository.dart'; import 'package:flux/features/settings/settings.dart'; void main() async { @@ -52,7 +51,7 @@ void main() async { BlocProvider(create: (_) => CustomerCubit()), BlocProvider(create: (_) => ProductCubit()), BlocProvider(create: (_) => StaffCubit()), - BlocProvider(create: (_) => ServicesCubit()), + BlocProvider(create: (_) => OperationsCubit()), BlocProvider(create: (_) => ProvidersCubit()), ], child: const FluxApp(), @@ -85,7 +84,9 @@ Future setupLocator() async { getIt.registerLazySingleton(() => CustomerRepository()); getIt.registerLazySingleton(() => ProductRepository()); getIt.registerLazySingleton(() => StaffRepository()); - getIt.registerLazySingleton(() => ServicesRepository()); + getIt.registerLazySingleton( + () => OperationsRepository(), + ); getIt.registerLazySingleton(() => ProviderRepository()); // NOTA: CompanyRepository l'ho tolto perché la logica della Company