reworked operation (#12)

Reviewed-on: #12
Co-authored-by: Mark M2 Macbook <marco@catelli.it>
Co-committed-by: Mark M2 Macbook <marco@catelli.it>
This commit is contained in:
2026-05-04 15:36:42 +02:00
committed by brontomark
parent 9f57207a39
commit 94ad524bae
110 changed files with 5831 additions and 5306 deletions

View File

@@ -0,0 +1,389 @@
import 'dart:async';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/attachments/models/attachment_model.dart';
import 'package:flux/features/operations/data/operations_repository.dart';
import 'package:get_it/get_it.dart';
import 'package:image_picker/image_picker.dart';
part 'operation_files_events.dart';
part 'operation_files_state.dart';
class OperationFilesBloc
extends Bloc<OperationFilesEvent, OperationFilesState> {
final _repository = GetIt.I.get<OperationsRepository>();
final String? operationId;
OperationFilesBloc({this.operationId})
: super(
OperationFilesState(
status: OperationFilesStatus.initial,
operationId: operationId,
),
) {
on<OperationsavedEvent>(_onOperationsaved);
on<LoadOperationFilesEvent>(_onLoadOperationFiles);
on<AddOperationFilesEvent>(_onAddOperationFiles);
on<UploadOperationFilesEvent>(_onUploadOperationFiles);
on<DeleteOperationFilesEvent>(_onDeleteOperationFiles);
on<ToggleOperationFileSelectionEvent>(_onToggleOperationFileSelection);
on<LinkFilesToCustomerEvent>(_onLinkFilesToCustomer);
on<RenameOperationFileEvent>(_onRenameOperationFile);
on<DeleteSpecificOperationFileEvent>(_onDeleteSpecificOperationFiles);
on<SelectAllOperationFilesEvent>(_onSelectAllOperationFiles);
on<ClearOperationFileSelectionEvent>(_onClearOperationFileSelection);
// Se il BLoC nasce con un ID, accendiamo subito lo stream!
if (operationId != null) {
add(LoadOperationFilesEvent(operationId: operationId));
}
}
FutureOr<void> _onOperationsaved(
OperationsavedEvent event,
Emitter<OperationFilesState> emit,
) async {
// 1. Aggiorniamo l'ID e mettiamo in loading
emit(
state.copyWith(
operationId: event.operationId,
status: OperationFilesStatus.uploading,
),
);
// 2. RECUPERO E UPLOAD DEI FILE "PARCHEGGIATI" (Pratica Nuova)
if (state.localFiles.isNotEmpty) {
try {
final List<Future<void>> uploadTasks = [];
for (var file in state.localFiles) {
// Ricreiamo il PlatformFile dal nostro AttachmentModel
// così il repository lo accetta senza fare storie!
final fakePlatformFile = PlatformFile(
name: '${file.name}.${file.extension}',
size: file.fileSize,
bytes: file.localBytes,
);
uploadTasks.add(
_repository.uploadAndRegisterOperationFile(
operationId: event.operationId, // L'ID APPENA NATO!
pickedFile: fakePlatformFile,
),
);
}
// Lanciamo tutti gli upload in parallelo
await Future.wait(uploadTasks);
} catch (e) {
emit(
state.copyWith(
status: OperationFilesStatus.failure,
error: "Errore upload post-salvataggio: $e",
),
);
return; // Ci fermiamo qui se esplode qualcosa
}
}
// 3. FINE DEI GIOCHI! Svuotiamo i locali, passiamo a success e accendiamo lo Stream
emit(state.copyWith(localFiles: [], status: OperationFilesStatus.success));
add(LoadOperationFilesEvent(operationId: event.operationId));
}
FutureOr<void> _onLoadOperationFiles(
LoadOperationFilesEvent event,
Emitter<OperationFilesState> emit,
) async {
final currentId = event.operationId ?? state.operationId;
if (currentId != null) {
emit(state.copyWith(status: OperationFilesStatus.loading));
await emit.forEach(
_repository.getOperationFilesStream(currentId),
onData: (List<AttachmentModel> data) => state.copyWith(
status: OperationFilesStatus.success,
remoteFiles: data,
),
onError: (error, stackTrace) => state.copyWith(
status: OperationFilesStatus.failure,
error: error.toString(),
),
);
}
}
void _onAddOperationFiles(
AddOperationFilesEvent event,
Emitter<OperationFilesState> emit,
) async {
final currentId = state.operationId;
// BIVIO 1: PRATICA NUOVA (Nessun ID - salvataggio locale)
if (currentId == null) {
final companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
final newLocalFiles = event.files.map((file) {
return AttachmentModel(
id: null,
companyId: companyId,
operationId: '', // Sarà riempito al salvataggio
name: file.name.fileNameWithoutExtension(),
extension: file.name.fileExtension(),
storagePath: '',
fileSize: file.size,
localBytes: file.bytes,
);
}).toList();
emit(
state.copyWith(
localFiles: [...state.localFiles, ...newLocalFiles],
status: OperationFilesStatus.success,
),
);
return;
}
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID - Upload immediato)
emit(state.copyWith(status: OperationFilesStatus.uploading));
try {
final List<Future<void>> uploadTasks = [];
for (var file in event.files) {
uploadTasks.add(
_repository.uploadAndRegisterOperationFile(
operationId: currentId,
pickedFile: file,
),
);
}
await Future.wait(uploadTasks);
emit(state.copyWith(status: OperationFilesStatus.success));
} catch (e) {
emit(
state.copyWith(
status: OperationFilesStatus.failure,
error: e.toString(),
),
);
}
}
FutureOr<void> _onUploadOperationFiles(
UploadOperationFilesEvent event,
Emitter<OperationFilesState> emit,
) async {
if ((event.pickedFiles == null || event.pickedFiles!.isEmpty) &&
(event.photos == null || event.photos!.isEmpty)) {
return;
}
if (state.operationId == null) return;
emit(state.copyWith(status: OperationFilesStatus.uploading));
try {
final List<Future<void>> uploadTasks = [];
// 1. Gestione Documenti normali (PlatformFile)
if (event.pickedFiles != null) {
for (var file in event.pickedFiles!) {
uploadTasks.add(
_repository.uploadAndRegisterOperationFile(
operationId: state.operationId!,
pickedFile: file,
),
);
}
}
// 2. Gestione Foto Fotocamera (XFile)
if (event.photos != null) {
for (var photo in event.photos!) {
// Leggiamo i byte asincronamente
final bytes = await photo.readAsBytes();
final fileSize = await photo.length();
// Lo travestiamo da PlatformFile per passarlo al Repository!
final fakePlatformFile = PlatformFile(
name: photo.name,
size: fileSize,
bytes: bytes,
path: photo.path,
);
uploadTasks.add(
_repository.uploadAndRegisterOperationFile(
operationId: state.operationId!,
pickedFile: fakePlatformFile,
),
);
}
}
// Esecuzione parallela di tutti i documenti e foto
await Future.wait(uploadTasks);
emit(state.copyWith(status: OperationFilesStatus.success));
} catch (e) {
emit(
state.copyWith(
status: OperationFilesStatus.failure,
error: e.toString(),
),
);
}
}
FutureOr<void> _onDeleteOperationFiles(
DeleteOperationFilesEvent event,
Emitter<OperationFilesState> 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<void> _onToggleOperationFileSelection(
ToggleOperationFileSelectionEvent event,
Emitter<OperationFilesState> emit,
) {
final selectedFiles = List<AttachmentModel>.from(state.selectedFiles);
if (selectedFiles.contains(event.file)) {
selectedFiles.remove(event.file);
} else {
selectedFiles.add(event.file);
}
emit(state.copyWith(selectedFiles: selectedFiles));
}
void _onSelectAllOperationFiles(
SelectAllOperationFilesEvent event,
Emitter<OperationFilesState> emit,
) {
// Prendiamo TUTTI i file (locali e remoti) e li buttiamo nei selezionati
emit(state.copyWith(selectedFiles: state.allFiles));
}
void _onClearOperationFileSelection(
ClearOperationFileSelectionEvent event,
Emitter<OperationFilesState> emit,
) {
// Svuotiamo brutalmente la lista
emit(state.copyWith(selectedFiles: []));
}
FutureOr<void> _onLinkFilesToCustomer(
LinkFilesToCustomerEvent event,
Emitter<OperationFilesState> emit,
) async {
if (state.selectedFiles.isEmpty) return;
// BIVIO 1: PRATICA NUOVA (Modalità Locale)
if (state.operationId == null) {
// Mappiamo i file locali: se sono tra quelli selezionati, iniettiamo il customerId
final updatedLocalFiles = state.localFiles.map((file) {
if (state.selectedFiles.contains(file)) {
return file.copyWith(customerId: event.customerId);
}
return file;
}).toList();
emit(
state.copyWith(
localFiles: updatedLocalFiles,
selectedFiles: [], // Svuotiamo la selezione dopo averli associati
status: OperationFilesStatus.success, // o un toast di feedback
),
);
return;
}
// BIVIO 2: PRATICA ESISTENTE (Modalità Remota su DB)
emit(state.copyWith(status: OperationFilesStatus.loading));
try {
final List<Future<void>> linkTasks = [];
for (var file in state.selectedFiles) {
linkTasks.add(
_repository.copyFileToCustomer(
file: file,
customerId: event.customerId,
),
);
}
await Future.wait(linkTasks);
// Svuotiamo la selezione.
// NON serve aggiornare la lista a mano, perché il DB si aggiorna
// e lo Stream di Supabase spingerà automaticamente in UI i file aggiornati!
emit(
state.copyWith(status: OperationFilesStatus.success, selectedFiles: []),
);
} catch (e) {
emit(
state.copyWith(
status: OperationFilesStatus.failure,
error: "Errore associazione: $e",
),
);
}
}
FutureOr<void> _onRenameOperationFile(
RenameOperationFileEvent event,
Emitter<OperationFilesState> emit,
) async {
// BIVIO 1: File Locale (Bozza)
if (event.file.localBytes != null) {
final updatedLocalFiles = state.localFiles.map((f) {
if (f == event.file) {
return f.copyWith(name: event.newName);
}
return f;
}).toList();
emit(state.copyWith(localFiles: updatedLocalFiles));
return;
}
// BIVIO 2: File Remoto (Salvato su DB)
emit(state.copyWith(status: OperationFilesStatus.loading));
try {
await _repository.renameAttachment(event.file.id!, event.newName);
emit(state.copyWith(status: OperationFilesStatus.success));
} catch (e) {
emit(
state.copyWith(
status: OperationFilesStatus.failure,
error: "Errore rinomina: $e",
),
);
}
}
FutureOr<void> _onDeleteSpecificOperationFiles(
DeleteSpecificOperationFileEvent event,
Emitter<OperationFilesState> emit,
) {
if (event.file.localBytes != null) {
final updatedLocalFiles = state.localFiles
.where((f) => f != event.file)
.toList();
emit(state.copyWith(localFiles: updatedLocalFiles));
}
}
}

View File

@@ -0,0 +1,81 @@
part of 'operation_files_bloc.dart';
abstract class OperationFilesEvent extends Equatable {
const OperationFilesEvent();
@override
List<Object?> get props => [];
}
class OperationsavedEvent extends OperationFilesEvent {
final String operationId;
const OperationsavedEvent(this.operationId);
@override
List<Object?> get props => [operationId];
}
class LoadOperationFilesEvent extends OperationFilesEvent {
final String? operationId;
final AttachmentModel? operation;
const LoadOperationFilesEvent({this.operationId, this.operation});
@override
List<Object?> get props => [operationId, operation];
}
class AddOperationFilesEvent extends OperationFilesEvent {
final List<PlatformFile> files;
const AddOperationFilesEvent(this.files);
@override
List<Object?> get props => [files];
}
class UploadOperationFilesEvent extends OperationFilesEvent {
final List<PlatformFile>? pickedFiles;
final List<XFile>? photos;
const UploadOperationFilesEvent({this.pickedFiles, this.photos});
@override
List<Object?> get props => [pickedFiles, photos];
}
class LinkFilesToCustomerEvent extends OperationFilesEvent {
final String customerId;
const LinkFilesToCustomerEvent({required this.customerId});
@override
List<Object?> get props => [customerId];
}
class DeleteOperationFilesEvent extends OperationFilesEvent {}
class ToggleOperationFileSelectionEvent extends OperationFilesEvent {
final AttachmentModel file;
const ToggleOperationFileSelectionEvent(this.file);
}
class RenameOperationFileEvent extends OperationFilesEvent {
final AttachmentModel file;
final String newName;
const RenameOperationFileEvent(this.file, this.newName);
@override
List<Object?> get props => [file, newName];
}
class DeleteSpecificOperationFileEvent extends OperationFilesEvent {
final AttachmentModel file;
const DeleteSpecificOperationFileEvent(this.file);
@override
List<Object?> get props => [file];
}
class SelectAllOperationFilesEvent extends OperationFilesEvent {}
class ClearOperationFileSelectionEvent extends OperationFilesEvent {}

View File

@@ -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<AttachmentModel> localFiles;
final List<AttachmentModel> remoteFiles;
final List<AttachmentModel> selectedFiles;
@override
List<Object?> get props => [
operationId,
status,
error,
localFiles,
remoteFiles,
selectedFiles,
];
List<AttachmentModel> get allFiles => [...remoteFiles, ...localFiles];
OperationFilesState copyWith({
String? operationId,
OperationFilesStatus? status,
String? error,
List<AttachmentModel>? localFiles,
List<AttachmentModel>? remoteFiles,
List<AttachmentModel>? 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,
);
}
}

View File

@@ -0,0 +1,304 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/operations/data/operations_repository.dart';
import 'package:flux/features/operations/models/operation_model.dart';
import 'package:get_it/get_it.dart';
import 'package:collection/collection.dart';
import 'package:uuid/uuid.dart';
part 'operations_state.dart';
class OperationsCubit extends Cubit<OperationsState> {
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
final Uuid _uuid = const Uuid(); // Generatore di UUID per il batch
OperationsCubit()
: super(const OperationsState(status: OperationsStatus.initial));
// --- CARICAMENTO E PAGINAZIONE ---
Future<void> loadOperations({bool refresh = false}) async {
if (state.status == OperationsStatus.loading) return;
if (!refresh && state.hasReachedMax) return;
emit(
state.copyWith(
status: OperationsStatus.loading,
errorMessage: null,
allOperations: refresh ? [] : state.allOperations,
hasReachedMax: refresh ? false : state.hasReachedMax,
),
);
try {
final currentOffset = refresh ? 0 : state.allOperations.length;
final companyId = _sessionCubit.state.company?.id;
if (companyId == null) {
throw Exception("Company ID non trovato nella sessione");
}
final newOperations = await _repository.fetchOperations(
companyId: companyId,
offset: currentOffset,
limit: 50,
searchTerm: state.query,
dateRange: state.dateRange,
);
final bool reachedMax = newOperations.length < 50;
emit(
state.copyWith(
status: OperationsStatus.ready,
allOperations: refresh
? newOperations
: [...state.allOperations, ...newOperations],
hasReachedMax: reachedMax,
),
);
} catch (e) {
emit(
state.copyWith(
status: OperationsStatus.failure,
errorMessage: "Errore nel caricamento operazioni: $e",
),
);
}
}
// --- GESTIONE FILTRI ---
void updateFilters({String? query, DateTimeRange? range}) {
emit(
state.copyWith(
query: query ?? state.query,
dateRange: range ?? state.dateRange,
),
);
loadOperations(refresh: true);
}
void clearFilters() {
emit(state.copyWith(query: '', dateRange: null));
loadOperations(refresh: true);
}
void initOperationForm({
OperationModel? existingOperation,
String? operationId,
String? staffId,
String? staffDisplayName,
}) async {
if (existingOperation != null) {
emit(
state.copyWith(
currentOperation: existingOperation,
status: OperationsStatus.ready,
),
);
} else if (operationId != null) {
OperationModel? operationModel = state.allOperations.firstWhereOrNull(
(s) => s.id == operationId,
);
operationModel ??= await _repository.fetchOperationById(operationId);
emit(
state.copyWith(
currentOperation: operationModel,
status: OperationsStatus.ready,
),
);
} else {
// NUOVA PRATICA: Creiamo un nuovo Batch UUID
emit(
state.copyWith(
currentOperation: OperationModel(
storeId: _sessionCubit.state.currentStore?.id ?? '',
reference: '',
createdAt: DateTime.now(),
companyId: _sessionCubit.state.company!.id!,
status: OperationStatus.draft,
batchUuid: _uuid.v4(), // <-- GENERIAMO IL BATCH UNIVOCO
),
status: OperationsStatus.ready,
),
);
}
}
/// MAGIA PURA: Prepara il form per inserire un altro servizio nella stessa pratica.
/// Mantiene il Cliente, il Batch e lo Store, ma svuota il resto.
void prepareNextOperationInBatch() {
if (state.currentOperation == null) return;
final current = state.currentOperation!;
emit(
state.copyWith(
status: OperationsStatus.ready,
currentOperation: OperationModel(
companyId: current.companyId,
storeId: current.storeId,
storeDisplayName: current.storeDisplayName,
batchUuid: current.batchUuid, // <-- MANTIENE IL COLLEGAMENTO
customerId: current.customerId, // <-- MANTIENE IL CLIENTE
customerDisplayName: current.customerDisplayName,
status: OperationStatus.draft,
createdAt: DateTime.now(),
),
),
);
}
// --- PERSISTENZA ---
Future<void> saveCurrentOperation({
required OperationStatus targetStatus,
bool shouldPop = true,
}) async {
if (state.currentOperation == null) return;
emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null));
try {
final operationToSave = state.currentOperation!.copyWith(
status: targetStatus,
);
final updatedOperation = await _repository.saveFullOperation(
operation: operationToSave,
);
emit(
state.copyWith(
// Se non facciamo Pop (es. l'utente vuole aggiungere un altro servizio), non killiamo l'operazione corrente
status: shouldPop
? OperationsStatus.saved
: OperationsStatus.savedNoPop,
currentOperation: shouldPop ? null : updatedOperation,
),
);
// Ricarica in background per la dashboard
loadOperations(refresh: true);
} catch (e) {
emit(
state.copyWith(
status: OperationsStatus.failure,
errorMessage: e.toString(),
),
);
}
}
// --- RECUPERO OPERAZIONI DELLO STESSO BATCH (Per UI di riepilogo) ---
/// Puoi usare questa funzione se nella UI vuoi mostrare "Hai inserito 3 servizi in questa pratica"
List<OperationModel> getOperationsInCurrentBatch() {
if (state.currentOperation == null) return [];
final currentBatch = state.currentOperation!.batchUuid;
// Filtriamo dalla lista caricata (o potresti fare una query diretta a Supabase se preferisci)
return state.allOperations
.where(
(op) =>
op.batchUuid == currentBatch &&
op.id != state.currentOperation!.id,
)
.toList();
}
// --- GESTIONE DELLO STATO DEL FORM IN TEMPO REALE ---
void updateOperationFields({
String? customerId,
String? customerDisplayName,
String? type,
String? providerId,
String? providerDisplayName,
String? subtype,
String? description,
DateTime? expirationDate,
int? quantity,
String? modelId,
String? modelDisplayName,
String? staffId,
String? staffDisplayName,
// Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo
bool clearProvider = false,
bool clearType = false,
bool clearSubtype = false,
bool clearDescription = false,
bool clearExpiration = false,
bool clearQuantity = false,
bool clearModel = false,
}) {
if (state.currentOperation == null) return;
final current = state.currentOperation!;
// Creiamo il modello aggiornato
// ATTENZIONE: adatta questa logica in base a come è scritto il tuo copyWith!
int? newQuantity;
if (clearQuantity) {
newQuantity = 1;
}
if (quantity != null && quantity <= 0) {
newQuantity = 0;
}
if (quantity != null && quantity > 0) {
newQuantity = quantity;
}
final updated = current.copyWith(
customerId: customerId,
customerDisplayName: customerDisplayName,
// Se clearProvider è true, forziamo una stringa vuota (o null se il tuo modello lo supporta)
providerId: clearProvider ? null : (providerId ?? current.providerId),
providerDisplayName: clearProvider
? null
: (providerDisplayName ?? current.providerDisplayName),
quantity: newQuantity,
type: clearType ? null : (type ?? current.type),
description: clearDescription
? null
: (description ?? current.description),
subtype: clearSubtype ? null : (subtype ?? current.subtype),
expirationDate: clearExpiration
? null
: (expirationDate ?? current.expirationDate),
modelId: clearModel ? null : (modelId ?? current.modelId),
modelDisplayName: clearModel
? null
: (modelDisplayName ?? current.modelDisplayName),
staffId: staffId ?? current.staffId,
staffDisplayName: staffDisplayName ?? current.staffDisplayName,
);
emit(state.copyWith(currentOperation: updated));
}
// Metodo di utilità per calcolare la data X mesi da oggi
DateTime _calculateMonths(int months) {
final now = DateTime.now();
return DateTime(now.year, now.month + months, now.day);
}
// Quando l'utente seleziona un tipo, impostiamo il default
void setTypeWithSmartDefault(String type) {
DateTime? defaultDate;
if (type == 'Energy') defaultDate = _calculateMonths(24);
if (type == 'Fin') defaultDate = _calculateMonths(30);
if (type == 'Entertainment') defaultDate = _calculateMonths(12);
updateOperationFields(
type: type,
expirationDate: defaultDate,
clearProvider: true,
clearSubtype: true,
clearModel: true,
clearQuantity: true,
);
}
}

View File

@@ -0,0 +1,68 @@
part of 'operations_cubit.dart';
enum OperationsStatus {
initial,
loading,
ready,
saving,
saved,
savedNoPop,
success,
failure,
}
class OperationsState extends Equatable {
final OperationsStatus status;
final List<OperationModel> allOperations;
final OperationModel? currentOperation; // La bozza che stiamo editando
final String? errorMessage;
final String query;
final DateTimeRange? dateRange;
final bool hasReachedMax;
final bool isSavingDraft;
const OperationsState({
required this.status,
this.allOperations = const [],
this.currentOperation,
this.errorMessage,
this.query = '',
this.dateRange,
this.hasReachedMax = false,
this.isSavingDraft = false,
});
OperationsState copyWith({
OperationsStatus? status,
List<OperationModel>? allOperations,
OperationModel? currentOperation,
String? errorMessage,
String? query,
DateTimeRange? dateRange,
bool? hasReachedMax,
bool? isSavingDraft,
}) {
return OperationsState(
status: status ?? this.status,
allOperations: allOperations ?? this.allOperations,
currentOperation: currentOperation ?? this.currentOperation,
errorMessage: errorMessage,
query: query ?? this.query,
dateRange: dateRange ?? this.dateRange,
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
isSavingDraft: isSavingDraft ?? this.isSavingDraft,
);
}
@override
List<Object?> get props => [
status,
allOperations,
currentOperation,
errorMessage,
query,
dateRange,
hasReachedMax,
isSavingDraft,
];
}