From f31ff19a746b0efd64fc54b44a0ff78ca51e8fd0 Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Sat, 30 May 2026 15:19:22 +0200 Subject: [PATCH] refactor dashboard operation list e task list with applifecycle --- .../dashboard_store_operation_list_cubit.dart | 82 ++++++++ .../dashboard_store_operation_list_state.dart | 30 +++ .../ui/latest_store_operations_card.dart | 39 +--- .../blocs/dashboard_task_list_cubit.dart | 38 ++-- .../bloc/latest_store_operations_bloc.dart | 66 ------ .../bloc/latest_store_operations_events.dart | 17 -- .../bloc/latest_store_operations_state.dart | 30 --- lib/features/home/ui/home_screen.dart | 191 ++++++++++++------ .../data/operations_repository.dart | 17 +- 9 files changed, 282 insertions(+), 228 deletions(-) create mode 100644 lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart create mode 100644 lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_state.dart rename lib/features/home/{latest_store_operations => dashboard_store_operation_list}/ui/latest_store_operations_card.dart (82%) delete mode 100644 lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart delete mode 100644 lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart delete mode 100644 lib/features/home/latest_store_operations/bloc/latest_store_operations_state.dart diff --git a/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart b/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart new file mode 100644 index 0000000..c939a2a --- /dev/null +++ b/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart @@ -0,0 +1,82 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.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 'dashboard_store_operation_list_state.dart'; + +class DashboardStoreOperationListCubit + extends Cubit { + final OperationsRepository _repository = GetIt.I.get(); + final String? companyId; + final String? storeId; + StreamSubscription? _operationsSubscription; + DashboardStoreOperationListCubit({ + required this.companyId, + required this.storeId, + }) : super( + const DashboardStoreOperationListState( + status: DashboardStoreOperationListStatus.initial, + ), + ); + + void startListening() { + emit(state.copyWith(status: DashboardStoreOperationListStatus.loading)); + stopListening(); + + // Primo caricamento + _loadOperationsSilently(); + + // Inizio ascolto campanello + try { + _operationsSubscription = _repository + .watchStoreOperations(storeId: storeId!, limit: 10) + .listen((_) { + // Quando il campanello suona (qualcosa è cambiato a DB), ricarichiamo! + _loadOperationsSilently(); + }); + } on Exception catch (e) { + debugPrint(e.toString()); + } + } + + void stopListening() { + _operationsSubscription?.cancel(); + _operationsSubscription = null; + } + + void _loadOperationsSilently() async { + try { + final operations = await _repository.fetchOperations( + companyId: companyId!, + storeId: storeId!, + limit: 10, + offset: 0, + ); + emit( + state.copyWith( + status: DashboardStoreOperationListStatus.success, + operations: operations, + error: null, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: DashboardStoreOperationListStatus.failure, + error: e.toString(), + ), + ); + } + } + + @override + Future close() { + stopListening(); + return super.close(); + } +} diff --git a/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_state.dart b/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_state.dart new file mode 100644 index 0000000..a7e44ab --- /dev/null +++ b/lib/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_state.dart @@ -0,0 +1,30 @@ +part of 'dashboard_store_operation_list_cubit.dart'; + +enum DashboardStoreOperationListStatus { initial, loading, success, failure } + +class DashboardStoreOperationListState extends Equatable { + final DashboardStoreOperationListStatus status; + final String? error; + final List operations; + + const DashboardStoreOperationListState({ + required this.status, + this.error, + this.operations = const [], + }); + + @override + List get props => [status, error, operations]; + + DashboardStoreOperationListState copyWith({ + DashboardStoreOperationListStatus? status, + String? error, + List? operations, + }) { + return DashboardStoreOperationListState( + 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/dashboard_store_operation_list/ui/latest_store_operations_card.dart similarity index 82% rename from lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart rename to lib/features/home/dashboard_store_operation_list/ui/latest_store_operations_card.dart index 3f871a5..22f3240 100644 --- a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart +++ b/lib/features/home/dashboard_store_operation_list/ui/latest_store_operations_card.dart @@ -1,38 +1,17 @@ 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/routes/routes.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:flux/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart'; import 'package:go_router/go_router.dart'; -class LatestStoreOperationsCard extends StatelessWidget { - const LatestStoreOperationsCard({super.key}); +class DashboardStoreOperationListCard extends StatelessWidget { + const DashboardStoreOperationListCard({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(InitLatestStoreOperationsEvent(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( - InitLatestStoreOperationsEvent(state.currentStore!.id!), - ); - } - }, - child: _LatestOperationsCardContent(), - ), - ); + return _LatestOperationsCardContent(); } } @@ -91,21 +70,21 @@ class _LatestOperationsCardContent extends StatelessWidget { Expanded( child: BlocBuilder< - LatestStoreOperationsBloc, - LatestStoreOperationsState + DashboardStoreOperationListCubit, + DashboardStoreOperationListState >( builder: (context, state) { if (state.status == - LatestStoreOperationsStatus.loading || + DashboardStoreOperationListStatus.loading || state.status == - LatestStoreOperationsStatus.initial) { + DashboardStoreOperationListStatus.initial) { return const Center( child: CircularProgressIndicator(), ); } if (state.status == - LatestStoreOperationsStatus.failure) { + DashboardStoreOperationListStatus.failure) { return Center( child: Text( "Errore di caricamento", diff --git a/lib/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart b/lib/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart index 062497d..b48a722 100644 --- a/lib/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart +++ b/lib/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/tasks/data/task_repository.dart'; import 'package:flux/features/tasks/models/task_model.dart'; @@ -11,37 +12,41 @@ part 'dashboard_task_list_state.dart'; class DashboardTaskListCubit extends Cubit { final TasksRepository _repository = GetIt.I.get(); - final String staffId; - final String companyId; - StreamSubscription? _taskSubscription; + final String? staffId; + final String? companyId; + StreamSubscription? _tasksSubscription; DashboardTaskListCubit({required this.staffId, required this.companyId}) - : super(const DashboardTaskListState()) { - _initRealtime(); - } + : super(const DashboardTaskListState()); - void _initRealtime() { + void startListening() { emit(state.copyWith(status: DashboardTaskListStatus.loading)); // Primo caricamento _loadTasksSilently(); // Inizio ascolto campanello - _taskSubscription = _repository.watchCompanyTasks(companyId).listen((_) { - // Quando il campanello suona (qualcosa è cambiato a DB), ricarichiamo! - _loadTasksSilently(); - }); + try { + _tasksSubscription = _repository.watchCompanyTasks(companyId!).listen(( + _, + ) { + // Quando il campanello suona (qualcosa è cambiato a DB), ricarichiamo! + _loadTasksSilently(); + }); + } on Exception catch (e) { + debugPrint(e.toString()); + } } - Future loadTasks() async { - emit(state.copyWith(status: DashboardTaskListStatus.loading)); - await _loadTasksSilently(); + void stopListening() { + _tasksSubscription?.cancel(); + _tasksSubscription = null; } Future _loadTasksSilently() async { try { final tasks = await _repository.getTasks( - companyId: companyId, + companyId: companyId!, staffId: staffId, statuses: [TaskStatus.open, TaskStatus.inProgress], limit: 10, @@ -66,8 +71,7 @@ class DashboardTaskListCubit extends Cubit { @override Future close() { - // Stacchiamo l'abbonamento. Il controller.onCancel nel Repo farà il resto! - _taskSubscription?.cancel(); + stopListening(); return super.close(); } } 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 deleted file mode 100644 index 55f088a..0000000 --- a/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart +++ /dev/null @@ -1,66 +0,0 @@ -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 - .getLatestStoreOperationsStream(storeId: event.storeId, limit: 10) - .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 deleted file mode 100644 index e7b4061..0000000 --- a/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart +++ /dev/null @@ -1,17 +0,0 @@ -part of 'latest_store_operations_bloc.dart'; - -sealed class LatestStoreOperationsEvent extends Equatable { - const LatestStoreOperationsEvent(); - - @override - List get props => []; -} - -class InitLatestStoreOperationsEvent extends LatestStoreOperationsEvent { - final String storeId; - - const InitLatestStoreOperationsEvent(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 deleted file mode 100644 index d373848..0000000 --- a/lib/features/home/latest_store_operations/bloc/latest_store_operations_state.dart +++ /dev/null @@ -1,30 +0,0 @@ -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/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 644a4a8..2e77d59 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -5,8 +5,10 @@ import 'package:flux/core/routes/routes.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/widgets/staff_selector_modal.dart'; +import 'package:flux/features/home/dashboard_store_operation_list/bloc/dashboard_store_operation_list_cubit.dart'; +import 'package:flux/features/home/dashboard_task_list/blocs/dashboard_task_list_cubit.dart'; import 'package:flux/features/home/dashboard_task_list/ui/dashboard_tasks_card.dart'; -import 'package:flux/features/home/latest_store_operations/ui/latest_store_operations_card.dart'; +import 'package:flux/features/home/dashboard_store_operation_list/ui/latest_store_operations_card.dart'; import 'package:flux/features/home/latest_store_tickets/ui/latest_store_tickets_card.dart'; import 'package:flux/features/home/ui/quick_actions_widget.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; @@ -17,85 +19,140 @@ import 'package:flux/features/notes/ui/dashboard_notes_widget.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; -class HomeScreen extends StatelessWidget { +class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + late final AppLifecycleListener _lifecycleListener; + + @override + void initState() { + super.initState(); + + // Inizializziamo il sensore del ciclo di vita + _lifecycleListener = AppLifecycleListener( + onPause: () { + // L'utente ha messo l'app in background (es. per rispondere a un messaggio su WhatsApp) + // Chiudiamo i rubinetti per non sprecare risorse e prevenire crash + context.read().stopListening(); + context.read().stopListening(); + debugPrint('App in background: Stream sospesi.'); + }, + onResume: () { + // L'utente è tornato sull'app! + // Riappriamo i rubinetti, Supabase ricreerà una connessione fresca + context.read().startListening(); + context.read().startListening(); + debugPrint('App in foreground: Stream riattivati.'); + }, + ); + + // Facciamo partire gli stream la primissima volta che la schermata si carica + context.read().startListening(); + context.read().startListening(); + } + + @override + void dispose() { + // Pulizia fondamentale + _lifecycleListener.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); + final sessionCubit = GetIt.I.get(); - return Scaffold( - backgroundColor: theme.colorScheme.surface, - body: SafeArea( - child: Column( - children: [ - // ========================================== - // 1. HEADER FISSO (Non scrolla mai) - // ========================================== - Container( - padding: const EdgeInsets.all(24.0), - // Un leggero colore di sfondo aiuta a staccare l'header quando il contenuto ci passa sotto - color: theme.colorScheme.surface, - child: _buildHeader(context, theme), - ), - - // ========================================== - // 2. CORPO DELLA DASHBOARD (Scrollabile) - // ========================================== - Expanded( - child: CustomScrollView( - slivers: [ - // --- QUICK ACTIONS: AZIONI RAPIDE --- - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: _buildQuickActions(context), - ), - ), - - const SliverToBoxAdapter(child: SizedBox(height: 32)), - - // --- I WIDGET DELLA DASHBOARD (Responsive Grid) --- - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 24.0), - sliver: SliverGrid( - gridDelegate: - const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 500, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - childAspectRatio: 1.3, - ), - delegate: SliverChildListDelegate([ - LatestStoreOperationsCard(), - LatestStoreTicketsCard(), - _buildDashboardWidget( - title: context.l10n.homeExpiringContracts, - icon: Icons.assignment_late_outlined, - color: Colors.orange, - context: context, - ), - DashboardNotesWidget(), - DashboardTasksCard(), - ]), - ), - ), - - // Spazio finale per non far attaccare l'ultima card al fondo - const SliverToBoxAdapter(child: SizedBox(height: 40)), - ], + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => DashboardStoreOperationListCubit( + companyId: sessionCubit.state.company?.id, + storeId: sessionCubit.state.currentStore?.id, + ), + ), + BlocProvider( + create: (context) => DashboardTaskListCubit( + companyId: sessionCubit.state.company?.id, + staffId: sessionCubit.state.currentStaffMember?.id, + ), + ), + ], + child: Scaffold( + backgroundColor: theme.colorScheme.surface, + body: SafeArea( + child: Column( + children: [ + // ========================================== + // 1. HEADER FISSO (Non scrolla mai) + // ========================================== + Container( + padding: const EdgeInsets.all(24.0), + // Un leggero colore di sfondo aiuta a staccare l'header quando il contenuto ci passa sotto + color: theme.colorScheme.surface, + child: _buildHeader(context, theme), ), - ), - ], + + // ========================================== + // 2. CORPO DELLA DASHBOARD (Scrollabile) + // ========================================== + Expanded( + child: CustomScrollView( + slivers: [ + // --- QUICK ACTIONS: AZIONI RAPIDE --- + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: _buildQuickActions(context), + ), + ), + + const SliverToBoxAdapter(child: SizedBox(height: 32)), + + // --- I WIDGET DELLA DASHBOARD (Responsive Grid) --- + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + sliver: SliverGrid( + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 500, + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 1.3, + ), + delegate: SliverChildListDelegate([ + DashboardStoreOperationListCard(), + LatestStoreTicketsCard(), + _buildDashboardWidget( + title: context.l10n.homeExpiringContracts, + icon: Icons.assignment_late_outlined, + color: Colors.orange, + context: context, + ), + DashboardNotesWidget(), + DashboardTasksCard(), + ]), + ), + ), + + // Spazio finale per non far attaccare l'ultima card al fondo + const SliverToBoxAdapter(child: SizedBox(height: 40)), + ], + ), + ), + ], + ), ), ), ); } // ========================================== - // WIDGET BUILDERS - // ========================================== - Widget _buildHeader(BuildContext context, ThemeData theme) { final user = context.watch().state.currentStaffMember; final currentStore = context.watch().state.currentStore; diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index 780fcce..491168a 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -38,6 +38,9 @@ class OperationsRepository { // --- RECUPERO PAGINATO CON FILTRI E JOIN --- Future> fetchOperations({ required String companyId, + String? storeId, + String? staffId, + String? providerId, required int offset, int limit = 50, String? searchTerm, @@ -64,6 +67,18 @@ class OperationsRepository { .lte('created_at', dateRange.end.toIso8601String()); } + if (storeId != null) { + query = query.or('store_id.eq.$storeId,store_id.is.null'); + } + + if (staffId != null) { + query = query.or('staff_id.eq.$staffId,staff_id.is.null'); + } + + if (providerId != null) { + query = query.or('provider_id.eq.$providerId,provider_id.is.null'); + } + if (searchTerm != null && searchTerm.isNotEmpty) { // Filtra sui campi della tabella principale O su quelli della tabella joinata query = query.or( @@ -83,7 +98,7 @@ class OperationsRepository { } } - Stream> getLatestStoreOperationsStream({ + Stream> watchStoreOperations({ required String storeId, required int limit, }) {