refactor dashboard operation list e task list with applifecycle
This commit is contained in:
@@ -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<DashboardStoreOperationListState> {
|
||||
final OperationsRepository _repository = GetIt.I.get<OperationsRepository>();
|
||||
final String? companyId;
|
||||
final String? storeId;
|
||||
StreamSubscription<void>? _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<void> close() {
|
||||
stopListening();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -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<OperationModel> operations;
|
||||
|
||||
const DashboardStoreOperationListState({
|
||||
required this.status,
|
||||
this.error,
|
||||
this.operations = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, error, operations];
|
||||
|
||||
DashboardStoreOperationListState copyWith({
|
||||
DashboardStoreOperationListStatus? status,
|
||||
String? error,
|
||||
List<OperationModel>? operations,
|
||||
}) {
|
||||
return DashboardStoreOperationListState(
|
||||
status: status ?? this.status,
|
||||
error: error,
|
||||
operations: operations ?? this.operations,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<SessionCubit>().state.currentStore?.id;
|
||||
|
||||
return BlocProvider(
|
||||
// 1. Creiamo il Bloc e facciamo partire subito la query
|
||||
create: (context) =>
|
||||
LatestStoreOperationsBloc()
|
||||
..add(InitLatestStoreOperationsEvent(currentStoreId ?? '')),
|
||||
child: BlocListener<SessionCubit, SessionState>(
|
||||
// 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<LatestStoreOperationsBloc>().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",
|
||||
@@ -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<DashboardTaskListState> {
|
||||
final TasksRepository _repository = GetIt.I.get<TasksRepository>();
|
||||
final String staffId;
|
||||
final String companyId;
|
||||
StreamSubscription<void>? _taskSubscription;
|
||||
final String? staffId;
|
||||
final String? companyId;
|
||||
StreamSubscription<void>? _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((_) {
|
||||
try {
|
||||
_tasksSubscription = _repository.watchCompanyTasks(companyId!).listen((
|
||||
_,
|
||||
) {
|
||||
// Quando il campanello suona (qualcosa è cambiato a DB), ricarichiamo!
|
||||
_loadTasksSilently();
|
||||
});
|
||||
} on Exception catch (e) {
|
||||
debugPrint(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadTasks() async {
|
||||
emit(state.copyWith(status: DashboardTaskListStatus.loading));
|
||||
await _loadTasksSilently();
|
||||
void stopListening() {
|
||||
_tasksSubscription?.cancel();
|
||||
_tasksSubscription = null;
|
||||
}
|
||||
|
||||
Future<void> _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<DashboardTaskListState> {
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
// Stacchiamo l'abbonamento. Il controller.onCancel nel Repo farà il resto!
|
||||
_taskSubscription?.cancel();
|
||||
stopListening();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LatestStoreOperationsEvent, LatestStoreOperationsState> {
|
||||
final _repository = GetIt.I.get<OperationsRepository>();
|
||||
|
||||
LatestStoreOperationsBloc()
|
||||
: super(
|
||||
const LatestStoreOperationsState(
|
||||
status: LatestStoreOperationsStatus.initial,
|
||||
),
|
||||
) {
|
||||
on<InitLatestStoreOperationsEvent>((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<OperationModel> rawOperations) async {
|
||||
// Questo gira ad ogni "scatto" dello stream di Supabase
|
||||
List<OperationModel> 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<OperationModel> 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
part of 'latest_store_operations_bloc.dart';
|
||||
|
||||
sealed class LatestStoreOperationsEvent extends Equatable {
|
||||
const LatestStoreOperationsEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class InitLatestStoreOperationsEvent extends LatestStoreOperationsEvent {
|
||||
final String storeId;
|
||||
|
||||
const InitLatestStoreOperationsEvent(this.storeId);
|
||||
|
||||
@override
|
||||
List<Object> get props => [storeId];
|
||||
}
|
||||
@@ -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<OperationModel> operations;
|
||||
|
||||
const LatestStoreOperationsState({
|
||||
required this.status,
|
||||
this.error,
|
||||
this.operations = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, error, operations];
|
||||
|
||||
LatestStoreOperationsState copyWith({
|
||||
LatestStoreOperationsStatus? status,
|
||||
String? error,
|
||||
List<OperationModel>? operations,
|
||||
}) {
|
||||
return LatestStoreOperationsState(
|
||||
status: status ?? this.status,
|
||||
error: error,
|
||||
operations: operations ?? this.operations,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,14 +19,71 @@ 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<HomeScreen> createState() => _HomeScreenState();
|
||||
}
|
||||
|
||||
class _HomeScreenState extends State<HomeScreen> {
|
||||
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<DashboardStoreOperationListCubit>().stopListening();
|
||||
context.read<DashboardTaskListCubit>().stopListening();
|
||||
debugPrint('App in background: Stream sospesi.');
|
||||
},
|
||||
onResume: () {
|
||||
// L'utente è tornato sull'app!
|
||||
// Riappriamo i rubinetti, Supabase ricreerà una connessione fresca
|
||||
context.read<DashboardStoreOperationListCubit>().startListening();
|
||||
context.read<DashboardTaskListCubit>().startListening();
|
||||
debugPrint('App in foreground: Stream riattivati.');
|
||||
},
|
||||
);
|
||||
|
||||
// Facciamo partire gli stream la primissima volta che la schermata si carica
|
||||
context.read<DashboardStoreOperationListCubit>().startListening();
|
||||
context.read<DashboardTaskListCubit>().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<SessionCubit>();
|
||||
|
||||
return Scaffold(
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<DashboardStoreOperationListCubit>(
|
||||
create: (context) => DashboardStoreOperationListCubit(
|
||||
companyId: sessionCubit.state.company?.id,
|
||||
storeId: sessionCubit.state.currentStore?.id,
|
||||
),
|
||||
),
|
||||
BlocProvider<DashboardTaskListCubit>(
|
||||
create: (context) => DashboardTaskListCubit(
|
||||
companyId: sessionCubit.state.company?.id,
|
||||
staffId: sessionCubit.state.currentStaffMember?.id,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Scaffold(
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
@@ -67,7 +126,7 @@ class HomeScreen extends StatelessWidget {
|
||||
childAspectRatio: 1.3,
|
||||
),
|
||||
delegate: SliverChildListDelegate([
|
||||
LatestStoreOperationsCard(),
|
||||
DashboardStoreOperationListCard(),
|
||||
LatestStoreTicketsCard(),
|
||||
_buildDashboardWidget(
|
||||
title: context.l10n.homeExpiringContracts,
|
||||
@@ -89,13 +148,11 @@ class HomeScreen extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// WIDGET BUILDERS
|
||||
// ==========================================
|
||||
|
||||
Widget _buildHeader(BuildContext context, ThemeData theme) {
|
||||
final user = context.watch<SessionCubit>().state.currentStaffMember;
|
||||
final currentStore = context.watch<SessionCubit>().state.currentStore;
|
||||
|
||||
@@ -38,6 +38,9 @@ class OperationsRepository {
|
||||
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
|
||||
Future<List<OperationModel>> 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<List<OperationModel>> getLatestStoreOperationsStream({
|
||||
Stream<List<OperationModel>> watchStoreOperations({
|
||||
required String storeId,
|
||||
required int limit,
|
||||
}) {
|
||||
|
||||
Reference in New Issue
Block a user