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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/routes/routes.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/core/utils/extensions.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';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class LatestStoreOperationsCard extends StatelessWidget {
|
class DashboardStoreOperationListCard extends StatelessWidget {
|
||||||
const LatestStoreOperationsCard({super.key});
|
const DashboardStoreOperationListCard({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final currentStoreId = context.read<SessionCubit>().state.currentStore?.id;
|
return _LatestOperationsCardContent();
|
||||||
|
|
||||||
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(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,21 +70,21 @@ class _LatestOperationsCardContent extends StatelessWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child:
|
child:
|
||||||
BlocBuilder<
|
BlocBuilder<
|
||||||
LatestStoreOperationsBloc,
|
DashboardStoreOperationListCubit,
|
||||||
LatestStoreOperationsState
|
DashboardStoreOperationListState
|
||||||
>(
|
>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.status ==
|
if (state.status ==
|
||||||
LatestStoreOperationsStatus.loading ||
|
DashboardStoreOperationListStatus.loading ||
|
||||||
state.status ==
|
state.status ==
|
||||||
LatestStoreOperationsStatus.initial) {
|
DashboardStoreOperationListStatus.initial) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CircularProgressIndicator(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status ==
|
if (state.status ==
|
||||||
LatestStoreOperationsStatus.failure) {
|
DashboardStoreOperationListStatus.failure) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"Errore di caricamento",
|
"Errore di caricamento",
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/features/tasks/data/task_repository.dart';
|
import 'package:flux/features/tasks/data/task_repository.dart';
|
||||||
import 'package:flux/features/tasks/models/task_model.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> {
|
class DashboardTaskListCubit extends Cubit<DashboardTaskListState> {
|
||||||
final TasksRepository _repository = GetIt.I.get<TasksRepository>();
|
final TasksRepository _repository = GetIt.I.get<TasksRepository>();
|
||||||
final String staffId;
|
final String? staffId;
|
||||||
final String companyId;
|
final String? companyId;
|
||||||
StreamSubscription<void>? _taskSubscription;
|
StreamSubscription<void>? _tasksSubscription;
|
||||||
|
|
||||||
DashboardTaskListCubit({required this.staffId, required this.companyId})
|
DashboardTaskListCubit({required this.staffId, required this.companyId})
|
||||||
: super(const DashboardTaskListState()) {
|
: super(const DashboardTaskListState());
|
||||||
_initRealtime();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initRealtime() {
|
void startListening() {
|
||||||
emit(state.copyWith(status: DashboardTaskListStatus.loading));
|
emit(state.copyWith(status: DashboardTaskListStatus.loading));
|
||||||
|
|
||||||
// Primo caricamento
|
// Primo caricamento
|
||||||
_loadTasksSilently();
|
_loadTasksSilently();
|
||||||
|
|
||||||
// Inizio ascolto campanello
|
// Inizio ascolto campanello
|
||||||
_taskSubscription = _repository.watchCompanyTasks(companyId).listen((_) {
|
try {
|
||||||
// Quando il campanello suona (qualcosa è cambiato a DB), ricarichiamo!
|
_tasksSubscription = _repository.watchCompanyTasks(companyId!).listen((
|
||||||
_loadTasksSilently();
|
_,
|
||||||
});
|
) {
|
||||||
|
// Quando il campanello suona (qualcosa è cambiato a DB), ricarichiamo!
|
||||||
|
_loadTasksSilently();
|
||||||
|
});
|
||||||
|
} on Exception catch (e) {
|
||||||
|
debugPrint(e.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadTasks() async {
|
void stopListening() {
|
||||||
emit(state.copyWith(status: DashboardTaskListStatus.loading));
|
_tasksSubscription?.cancel();
|
||||||
await _loadTasksSilently();
|
_tasksSubscription = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadTasksSilently() async {
|
Future<void> _loadTasksSilently() async {
|
||||||
try {
|
try {
|
||||||
final tasks = await _repository.getTasks(
|
final tasks = await _repository.getTasks(
|
||||||
companyId: companyId,
|
companyId: companyId!,
|
||||||
staffId: staffId,
|
staffId: staffId,
|
||||||
statuses: [TaskStatus.open, TaskStatus.inProgress],
|
statuses: [TaskStatus.open, TaskStatus.inProgress],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
@@ -66,8 +71,7 @@ class DashboardTaskListCubit extends Cubit<DashboardTaskListState> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
// Stacchiamo l'abbonamento. Il controller.onCancel nel Repo farà il resto!
|
stopListening();
|
||||||
_taskSubscription?.cancel();
|
|
||||||
return super.close();
|
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/theme/theme.dart';
|
||||||
import 'package:flux/core/utils/extensions.dart';
|
import 'package:flux/core/utils/extensions.dart';
|
||||||
import 'package:flux/core/widgets/staff_selector_modal.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/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/latest_store_tickets/ui/latest_store_tickets_card.dart';
|
||||||
import 'package:flux/features/home/ui/quick_actions_widget.dart';
|
import 'package:flux/features/home/ui/quick_actions_widget.dart';
|
||||||
import 'package:flux/features/master_data/staff/blocs/staff_cubit.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:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class HomeScreen extends StatelessWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
const HomeScreen({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final sessionCubit = GetIt.I.get<SessionCubit>();
|
||||||
|
|
||||||
return Scaffold(
|
return MultiBlocProvider(
|
||||||
backgroundColor: theme.colorScheme.surface,
|
providers: [
|
||||||
body: SafeArea(
|
BlocProvider<DashboardStoreOperationListCubit>(
|
||||||
child: Column(
|
create: (context) => DashboardStoreOperationListCubit(
|
||||||
children: [
|
companyId: sessionCubit.state.company?.id,
|
||||||
// ==========================================
|
storeId: sessionCubit.state.currentStore?.id,
|
||||||
// 1. HEADER FISSO (Non scrolla mai)
|
),
|
||||||
// ==========================================
|
),
|
||||||
Container(
|
BlocProvider<DashboardTaskListCubit>(
|
||||||
padding: const EdgeInsets.all(24.0),
|
create: (context) => DashboardTaskListCubit(
|
||||||
// Un leggero colore di sfondo aiuta a staccare l'header quando il contenuto ci passa sotto
|
companyId: sessionCubit.state.company?.id,
|
||||||
color: theme.colorScheme.surface,
|
staffId: sessionCubit.state.currentStaffMember?.id,
|
||||||
child: _buildHeader(context, theme),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
// ==========================================
|
child: Scaffold(
|
||||||
// 2. CORPO DELLA DASHBOARD (Scrollabile)
|
backgroundColor: theme.colorScheme.surface,
|
||||||
// ==========================================
|
body: SafeArea(
|
||||||
Expanded(
|
child: Column(
|
||||||
child: CustomScrollView(
|
children: [
|
||||||
slivers: [
|
// ==========================================
|
||||||
// --- QUICK ACTIONS: AZIONI RAPIDE ---
|
// 1. HEADER FISSO (Non scrolla mai)
|
||||||
SliverToBoxAdapter(
|
// ==========================================
|
||||||
child: Padding(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: _buildQuickActions(context),
|
// Un leggero colore di sfondo aiuta a staccare l'header quando il contenuto ci passa sotto
|
||||||
),
|
color: theme.colorScheme.surface,
|
||||||
),
|
child: _buildHeader(context, theme),
|
||||||
|
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
// ==========================================
|
||||||
|
// 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) {
|
Widget _buildHeader(BuildContext context, ThemeData theme) {
|
||||||
final user = context.watch<SessionCubit>().state.currentStaffMember;
|
final user = context.watch<SessionCubit>().state.currentStaffMember;
|
||||||
final currentStore = context.watch<SessionCubit>().state.currentStore;
|
final currentStore = context.watch<SessionCubit>().state.currentStore;
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ class OperationsRepository {
|
|||||||
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
|
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
|
||||||
Future<List<OperationModel>> fetchOperations({
|
Future<List<OperationModel>> fetchOperations({
|
||||||
required String companyId,
|
required String companyId,
|
||||||
|
String? storeId,
|
||||||
|
String? staffId,
|
||||||
|
String? providerId,
|
||||||
required int offset,
|
required int offset,
|
||||||
int limit = 50,
|
int limit = 50,
|
||||||
String? searchTerm,
|
String? searchTerm,
|
||||||
@@ -64,6 +67,18 @@ class OperationsRepository {
|
|||||||
.lte('created_at', dateRange.end.toIso8601String());
|
.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) {
|
if (searchTerm != null && searchTerm.isNotEmpty) {
|
||||||
// Filtra sui campi della tabella principale O su quelli della tabella joinata
|
// Filtra sui campi della tabella principale O su quelli della tabella joinata
|
||||||
query = query.or(
|
query = query.or(
|
||||||
@@ -83,7 +98,7 @@ class OperationsRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<OperationModel>> getLatestStoreOperationsStream({
|
Stream<List<OperationModel>> watchStoreOperations({
|
||||||
required String storeId,
|
required String storeId,
|
||||||
required int limit,
|
required int limit,
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
Reference in New Issue
Block a user