This commit is contained in:
2026-05-01 10:11:44 +02:00
parent 9c8576ada5
commit f8bcac51e1
48 changed files with 1187 additions and 1141 deletions

View File

@@ -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<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<UploadMultipleOperationFilesEvent>(_onUploadMultipleOperationFiles);
on<DeleteOperationFilesEvent>(_onDeleteOperationFiles);
on<ToggleOperationFileSelectionEvent>(_onToggleOperationFileSelection);
// 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,
) {
// 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<void> _onLoadOperationFiles(
LoadOperationFilesEvent event,
Emitter<OperationFilesState> 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<OperationFilesState> 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<OperationFileModel> 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<void> _onUploadOperationFiles(
UploadOperationFilesEvent event,
Emitter<OperationFilesState> 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<void> _onUploadMultipleOperationFiles(
UploadMultipleOperationFilesEvent event,
Emitter<OperationFilesState> 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<Future<void>> 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<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,
) {
List<OperationFileModel> selectedFiles = List.from(state.selectedFiles);
if (selectedFiles.contains(event.file)) {
selectedFiles.remove(event.file);
} else {
selectedFiles.add(event.file);
}
emit(state.copyWith(selectedFiles: selectedFiles));
}
}

View File

@@ -0,0 +1,56 @@
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 OperationModel? 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<File>? photos;
const UploadOperationFilesEvent({this.pickedFiles, this.photos});
@override
List<Object?> get props => [pickedFiles, photos];
}
class UploadMultipleOperationFilesEvent extends OperationFilesEvent {
final List<PlatformFile> files;
const UploadMultipleOperationFilesEvent(this.files);
@override
List<Object?> get props => [files];
}
class DeleteOperationFilesEvent extends OperationFilesEvent {}
class ToggleOperationFileSelectionEvent extends OperationFilesEvent {
final OperationFileModel file;
const ToggleOperationFileSelectionEvent(this.file);
}

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

@@ -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<ServicesState> {
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
class OperationsCubit extends Cubit<OperationsState> {
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial));
OperationsCubit()
: super(const OperationsState(status: OperationsStatus.initial));
// --- CARICAMENTO E PAGINAZIONE ---
Future<void> loadServices({bool refresh = false}) async {
Future<void> 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<ServicesState> {
);
// 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<ServicesState> {
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<ServicesState> {
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<ServicesState> {
customerDisplayName: customerDisplayName,
);
emit(state.copyWith(currentService: updated));
emit(state.copyWith(currentOperation: updated));
}
// --- GESTIONE MODULI COMPLESSI ---
void updateEnergyServices(List<EnergyServiceModel> energyList) {
void updateEnergyOperations(List<EnergyOperationModel> energyList) {
emit(
state.copyWith(
currentService: state.currentService?.copyWith(
energyServices: energyList,
currentOperation: state.currentOperation?.copyWith(
energyOperations: energyList,
),
),
);
}
void updateFinServices(List<FinServiceModel> finList) {
void updateFinOperations(List<FinOperationModel> finList) {
emit(
state.copyWith(
currentService: state.currentService?.copyWith(finServices: finList),
currentOperation: state.currentOperation?.copyWith(
finOperations: finList,
),
),
);
}
void updateEntertainmentServices(List<EntertainmentServiceModel> entList) {
void updateEntertainmentOperations(
List<EntertainmentOperationModel> entList,
) {
emit(
state.copyWith(
currentService: state.currentService?.copyWith(
entertainmentServices: entList,
currentOperation: state.currentOperation?.copyWith(
entertainmentOperations: entList,
),
),
);
@@ -202,36 +207,40 @@ class ServicesCubit extends Cubit<ServicesState> {
// --- PERSISTENZA ---
Future<void> saveCurrentService({
Future<void> saveCurrentOperation({
required bool isBozza,
bool shouldPop = true,
List<ServiceFileModel>? files,
List<OperationFileModel>? 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<ServicesState> {
void addAttachments(List<PlatformFile> 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<ServicesState> {
}).toList();
// Creiamo una nuova lista pulita
final List<ServiceFileModel> updatedList = [
...(state.currentService?.files ?? []),
final List<OperationFileModel> 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<ServiceFileModel>.from(
state.currentService!.files,
final updatedList = List<OperationFileModel>.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<ServiceFileModel> selectedFiles) async {
final currentService = state.currentService;
void saveAndCopyFileToCustomer(List<OperationFileModel> 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<ServicesState> {
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<ServicesState> {
// 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<ServicesState> {
// 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",
),
);

View File

@@ -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<ServiceModel> allServices;
final ServiceModel? currentService; // La bozza che stiamo editando
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 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<ServiceModel>? allServices,
ServiceModel? currentService,
OperationsState copyWith({
OperationsStatus? status,
List<OperationModel>? 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<Object?> get props => [
status,
allServices,
currentService,
allOperations,
currentOperation,
errorMessage,
query,
dateRange,

View File

@@ -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<ServiceFilesEvent, ServiceFilesState> {
final _repository = GetIt.I.get<ServicesRepository>();
final String? serviceId;
ServiceFilesBloc({this.serviceId})
: super(
ServiceFilesState(
status: ServiceFilesStatus.initial,
serviceId: serviceId,
),
) {
on<ServiceSavedEvent>(_onServiceSaved);
on<LoadServiceFilesEvent>(_onLoadServiceFiles);
on<AddServiceFilesEvent>(_onAddServiceFiles);
on<UploadServiceFilesEvent>(_onUploadServiceFiles);
on<UploadMultipleServiceFilesEvent>(_onUploadMultipleServiceFiles);
on<DeleteServiceFilesEvent>(_onDeleteServiceFiles);
on<ToggleServiceFileSelectionEvent>(_onToggleServiceFileSelection);
// Se il BLoC nasce con un ID, accendiamo subito lo stream!
if (serviceId != null) {
add(LoadServiceFilesEvent(serviceId: serviceId));
}
}
FutureOr<void> _onServiceSaved(
ServiceSavedEvent event,
Emitter<ServiceFilesState> 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<void> _onLoadServiceFiles(
LoadServiceFilesEvent event,
Emitter<ServiceFilesState> 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<ServiceFilesState> 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<ServiceFileModel> 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<void> _onUploadServiceFiles(
UploadServiceFilesEvent event,
Emitter<ServiceFilesState> 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<void> _onUploadMultipleServiceFiles(
UploadMultipleServiceFilesEvent event,
Emitter<ServiceFilesState> 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<Future<void>> 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<void> _onDeleteServiceFiles(
DeleteServiceFilesEvent event,
Emitter<ServiceFilesState> 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<void> _onToggleServiceFileSelection(
ToggleServiceFileSelectionEvent event,
Emitter<ServiceFilesState> emit,
) {
List<ServiceFileModel> selectedFiles = List.from(state.selectedFiles);
if (selectedFiles.contains(event.file)) {
selectedFiles.remove(event.file);
} else {
selectedFiles.add(event.file);
}
emit(state.copyWith(selectedFiles: selectedFiles));
}
}

View File

@@ -1,56 +0,0 @@
part of 'service_files_bloc.dart';
abstract class ServiceFilesEvent extends Equatable {
const ServiceFilesEvent();
@override
List<Object?> get props => [];
}
class ServiceSavedEvent extends ServiceFilesEvent {
final String serviceId;
const ServiceSavedEvent(this.serviceId);
@override
List<Object?> get props => [serviceId];
}
class LoadServiceFilesEvent extends ServiceFilesEvent {
final String? serviceId;
final ServiceModel? operation;
const LoadServiceFilesEvent({this.serviceId, this.operation});
@override
List<Object?> get props => [serviceId, operation];
}
class AddServiceFilesEvent extends ServiceFilesEvent {
final List<PlatformFile> files;
const AddServiceFilesEvent(this.files);
@override
List<Object?> get props => [files];
}
class UploadServiceFilesEvent extends ServiceFilesEvent {
final List<PlatformFile>? pickedFiles;
final List<File>? photos;
const UploadServiceFilesEvent({this.pickedFiles, this.photos});
@override
List<Object?> get props => [pickedFiles, photos];
}
class UploadMultipleServiceFilesEvent extends ServiceFilesEvent {
final List<PlatformFile> files;
const UploadMultipleServiceFilesEvent(this.files);
@override
List<Object?> get props => [files];
}
class DeleteServiceFilesEvent extends ServiceFilesEvent {}
class ToggleServiceFileSelectionEvent extends ServiceFilesEvent {
final ServiceFileModel file;
const ToggleServiceFileSelectionEvent(this.file);
}

View File

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

View File

@@ -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<SessionCubit>().state.company!.id;
final CustomerRepository _customerRepository = GetIt.I<CustomerRepository>();
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
Future<ServiceModel> fetchServiceById(String id) async {
Future<OperationModel> 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<List<ServiceModel>> fetchServices({
Future<List<OperationModel>> 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<List<ServiceModel>> getLastStoreServicesStream({
Stream<List<OperationModel>> 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<ServiceModel> saveFullService(ServiceModel operation) async {
Future<OperationModel> 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<Future> 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<void> deleteService(String id) async {
Future<void> 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<List<ServiceFileModel>> getServiceFilesStream(String serviceId) {
Stream<List<OperationFileModel>> 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<ServiceFileModel> uploadAndRegisterServiceFile({
required String serviceId,
Future<OperationFileModel> 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<void> 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<void> deleteServiceFiles(List<ServiceFileModel> files) async {
Future<void> deleteOperationFiles(List<OperationFileModel> files) async {
if (files.isEmpty) return;
// 1. Prepariamo le liste di ID e di Percorsi
final List<String> idsToDelete = files.map((f) => f.id!).toList();
final List<String> 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);

View File

@@ -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<String, dynamic> map) {
return EnergyServiceModel(
factory EnergyOperationModel.fromMap(Map<String, dynamic> 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,
};
}
}

View File

@@ -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<String, dynamic> map) {
return EntertainmentServiceModel(
factory EntertainmentOperationModel.fromMap(Map<String, dynamic> 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,
};
}

View File

@@ -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<Object?> get props => [id, createdAt, expiration, serviceId, modelId];
List<Object?> get props => [id, createdAt, expiration, operationId, modelId];
factory FinServiceModel.fromMap(Map<String, dynamic> map) {
return FinServiceModel(
factory FinOperationModel.fromMap(Map<String, dynamic> 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,
};

View File

@@ -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<String, dynamic> map) {
return ServiceFileModel(
factory OperationFileModel.fromMap(Map<String, dynamic> 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,
];

View File

@@ -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<EnergyServiceModel> energyServices;
final List<FinServiceModel> finServices;
final List<EntertainmentServiceModel> entertainmentServices;
final List<EnergyOperationModel> energyOperations;
final List<FinOperationModel> finOperations;
final List<EntertainmentOperationModel> entertainmentOperations;
// ALLEGATI (Aggiunto)
final List<ServiceFileModel> files;
final List<OperationFileModel> 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<EnergyServiceModel>? energyServices,
List<FinServiceModel>? finServices,
List<EntertainmentServiceModel>? entertainmentServices,
List<ServiceFileModel>? files, // <-- Aggiunto
List<EnergyOperationModel>? energyOperations,
List<FinOperationModel>? finOperations,
List<EntertainmentOperationModel>? entertainmentOperations,
List<OperationFileModel>? 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<String, dynamic> map) {
return ServiceModel(
factory OperationModel.fromMap(Map<String, dynamic> 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 [],

View File

@@ -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,

View File

@@ -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<ServiceFilesBloc>().add(AddServiceFilesEvent(result.files));
context.read<OperationFilesBloc>().add(
AddOperationFilesEvent(result.files),
);
}
}
@override
Widget build(BuildContext context) {
ServiceFilesBloc serviceFilesBloc = BlocProvider.of<ServiceFilesBloc>(
OperationFilesBloc operationFilesBloc = BlocProvider.of<OperationFilesBloc>(
context,
);
return BlocListener<ServicesCubit, ServicesState>(
return BlocListener<OperationsCubit, OperationsState>(
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<ServiceFilesBloc>().add(ServiceSavedEvent(newId));
final newId = state.currentOperation!.id!;
context.read<OperationFilesBloc>().add(OperationsavedEvent(newId));
},
child: BlocBuilder<ServiceFilesBloc, ServiceFilesState>(
child: BlocBuilder<OperationFilesBloc, OperationFilesState>(
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<void> _handleGenerateQr(BuildContext context) async {
final cubit = context.read<ServicesCubit>();
var currentService = cubit.state.currentService;
final cubit = context.read<OperationsCubit>();
var currentOperation = cubit.state.currentOperation;
// 1. CATTURIAMO IL BLOC MENTRE SIAMO ANCORA NELLA PAGINA
final serviceFilesBloc = context.read<ServiceFilesBloc>();
final operationFilesBloc = context.read<OperationFilesBloc>();
// 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<bool>(
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<ServiceFilesBloc, ServiceFilesState>(
child: BlocListener<OperationFilesBloc, OperationFilesState>(
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<ServiceFileModel> files,
List<OperationFileModel> 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<ServicesCubit>().saveAndCopyFileToCustomer(files);
context.read<OperationsCubit>().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,

View File

@@ -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});

View File

@@ -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<EnergyServiceModel> initialServices;
class EnergyOperationDialog extends StatefulWidget {
final List<EnergyOperationModel> 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<EnergyServiceDialog> createState() => _EnergyServiceDialogState();
State<EnergyOperationDialog> createState() => _EnergyOperationDialogState();
}
class _EnergyServiceDialogState extends State<EnergyServiceDialog> {
class _EnergyOperationDialogState extends State<EnergyOperationDialog> {
// Lista temporanea per non "sporcare" il cubit finché non si preme Conferma
late List<EnergyServiceModel> _tempList;
late List<EnergyOperationModel> _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<ProvidersCubit>().loadActiveProvidersForStore(
widget.currentStoreId,
@@ -52,9 +52,9 @@ class _EnergyServiceDialogState extends State<EnergyServiceDialog> {
// 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<EnergyServiceDialog> {
// VISTA 1: LA LISTA DEI CONTRATTI
// ==========================================
class _EnergyList extends StatelessWidget {
final List<EnergyServiceModel> operations;
final List<EnergyOperationModel> operations;
final List<ProviderModel>
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"),
),

View File

@@ -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<EntertainmentServiceModel> initialServices;
class EntertainmentOperationDialog extends StatefulWidget {
final List<EntertainmentOperationModel> initialOperations;
final String currentStoreId;
const EntertainmentServiceDialog({
const EntertainmentOperationDialog({
super.key,
required this.initialServices,
required this.initialOperations,
required this.currentStoreId,
});
@override
State<EntertainmentServiceDialog> createState() =>
_EntertainmentServiceDialogState();
State<EntertainmentOperationDialog> createState() =>
_EntertainmentOperationDialogState();
}
class _EntertainmentServiceDialogState
extends State<EntertainmentServiceDialog> {
late List<EntertainmentServiceModel> _tempList;
class _EntertainmentOperationDialogState
extends State<EntertainmentOperationDialog> {
late List<EntertainmentOperationModel> _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<ProvidersCubit>().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<EntertainmentServiceModel> operations;
final List<EntertainmentOperationModel> operations;
final List<ProviderModel> 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<List<String>>(
future: GetIt.I<ServicesRepository>().fetchTopEntertainmentTypes(
future: GetIt.I<OperationsRepository>().fetchTopEntertainmentTypes(
GetIt.I<SessionCubit>().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,

View File

@@ -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<FinServiceModel> initialServices;
class FinanceOperationDialog extends StatefulWidget {
final List<FinOperationModel> 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<FinanceServiceDialog> createState() => _FinanceServiceDialogState();
State<FinanceOperationDialog> createState() => _FinanceOperationDialogState();
}
class _FinanceServiceDialogState extends State<FinanceServiceDialog> {
late List<FinServiceModel> _tempList;
class _FinanceOperationDialogState extends State<FinanceOperationDialog> {
late List<FinOperationModel> _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<ProvidersCubit>().loadActiveProvidersForStore(
widget.currentStoreId,
@@ -109,7 +109,7 @@ class _FinanceServiceDialogState extends State<FinanceServiceDialog> {
// VISTA LISTA (STORICA)
// ===========================================================================
class _FinanceList extends StatelessWidget {
final List<FinServiceModel> operations;
final List<FinOperationModel> operations;
final List<ProviderModel> allProviders;
final List<ModelModel> 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(

View File

@@ -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<ServicesCubit>().updateField(number: val);
context.read<OperationsCubit>().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<ServicesCubit>().updateField(isBozza: val);
context.read<OperationsCubit>().updateField(isBozza: val);
},
),
),
@@ -79,7 +79,9 @@ class GeneralInfoSection extends StatelessWidget {
activeThumbColor: Colors.green,
contentPadding: EdgeInsets.zero,
onChanged: (val) {
context.read<ServicesCubit>().updateField(resultOk: val);
context.read<OperationsCubit>().updateField(
resultOk: val,
);
},
),
),
@@ -100,7 +102,7 @@ class GeneralInfoSection extends StatelessWidget {
alignLabelWithHint: true,
),
onChanged: (val) {
context.read<ServicesCubit>().updateField(note: val);
context.read<OperationsCubit>().updateField(note: val);
},
),
],

View File

@@ -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<ServiceFormScreen> createState() => _ServiceFormScreenState();
State<OperationFormScreen> createState() => _OperationFormScreenState();
}
class _ServiceFormScreenState extends State<ServiceFormScreen> {
class _OperationFormScreenState extends State<OperationFormScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// Diamo in pasto al Cubit tutto quello che abbiamo!
context.read<ServicesCubit>().initServiceForm(
existingService: widget.existingService,
serviceId: widget.serviceId,
context.read<OperationsCubit>().initOperationForm(
existingOperation: widget.existingOperation,
operationId: widget.operationId,
);
});
}
void _performSave(BuildContext context, {required bool isBozza}) {
FocusScope.of(context).unfocus();
context.read<ServicesCubit>().saveCurrentService(isBozza: isBozza);
context.read<OperationsCubit>().saveCurrentOperation(isBozza: isBozza);
}
@override
Widget build(BuildContext context) {
return BlocConsumer<ServicesCubit, ServicesState>(
return BlocConsumer<OperationsCubit, OperationsState>(
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<ServiceFormScreen> {
);
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<ServiceFormScreen> {
),
);
}
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<ServiceFormScreen> {
}
},
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<ServiceFormScreen> {
GeneralInfoSection(operation: operation),
const SizedBox(height: 24),
ServicesGrid(operation: operation),
OperationsGrid(operation: operation),
const SizedBox(height: 32),
AttachmentsSection(),

View File

@@ -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<ServiceMobileUploadScreen> createState() =>
_ServiceMobileUploadScreenState();
State<OperationMobileUploadScreen> createState() =>
_OperationMobileUploadScreenState();
}
class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
class _OperationMobileUploadScreenState
extends State<OperationMobileUploadScreen> {
// 1. LA NOSTRA STAGING AREA (Il "Carrello")
final List<PlatformFile> _stagedFiles = [];
@@ -35,10 +36,10 @@ class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
@override
Widget build(BuildContext context) {
return BlocListener<ServiceFilesBloc, ServiceFilesState>(
return BlocListener<OperationFilesBloc, OperationFilesState>(
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<ServiceMobileUploadScreen> {
);
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<ServiceMobileUploadScreen> {
},
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<ServiceMobileUploadScreen> {
// 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<ServiceFilesBloc>();
bloc.add(UploadMultipleServiceFilesEvent(_stagedFiles));
final bloc = context.read<OperationFilesBloc>();
bloc.add(UploadMultipleOperationFilesEvent(_stagedFiles));
// N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"!
}

View File

@@ -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<ServicesCubit>().updateField(al: val),
context.read<OperationsCubit>().updateField(al: val),
),
),
ActionCard(
@@ -73,7 +73,7 @@ class ServicesGrid extends StatelessWidget {
"MNP",
operation.mnp,
(val) =>
context.read<ServicesCubit>().updateField(mnp: val),
context.read<OperationsCubit>().updateField(mnp: val),
),
),
ActionCard(
@@ -86,7 +86,7 @@ class ServicesGrid extends StatelessWidget {
"NIP",
operation.nip,
(val) =>
context.read<ServicesCubit>().updateField(nip: val),
context.read<OperationsCubit>().updateField(nip: val),
),
),
ActionCard(
@@ -98,8 +98,9 @@ class ServicesGrid extends StatelessWidget {
context,
"Unica",
operation.unica,
(val) =>
context.read<ServicesCubit>().updateField(unica: val),
(val) => context.read<OperationsCubit>().updateField(
unica: val,
),
),
),
ActionCard(
@@ -111,7 +112,7 @@ class ServicesGrid extends StatelessWidget {
context,
"Telepass",
operation.telepass,
(val) => context.read<ServicesCubit>().updateField(
(val) => context.read<OperationsCubit>().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<List<EnergyServiceModel>>(
context: context,
builder: (context) => EnergyServiceDialog(
currentStoreId: operation.storeId,
initialServices: operation
.energyServices, // Passiamo la lista attuale
),
);
final result =
await showDialog<List<EnergyOperationModel>>(
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<ServicesCubit>().updateEnergyServices(
context.read<OperationsCubit>().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<List<FinServiceModel>>(
final result = await showDialog<List<FinOperationModel>>(
context: context,
builder: (context) => FinanceServiceDialog(
builder: (context) => FinanceOperationDialog(
productCubit: context.read<ProductCubit>(),
currentStoreId: operation.storeId,
initialServices: operation
.finServices, // Passiamo la lista attuale
initialOperations: operation
.finOperations, // Passiamo la lista attuale
),
);
if (result != null && context.mounted) {
context.read<ServicesCubit>().updateFinServices(result);
context.read<OperationsCubit>().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<List<EntertainmentServiceModel>>(
await showDialog<List<EntertainmentOperationModel>>(
context: context,
builder: (context) => EntertainmentServiceDialog(
initialServices: operation.entertainmentServices,
builder: (context) => EntertainmentOperationDialog(
initialOperations:
operation.entertainmentOperations,
currentStoreId: operation.storeId,
),
);
if (result != null && context.mounted) {
context
.read<ServicesCubit>()
.updateEntertainmentServices(result);
.read<OperationsCubit>()
.updateEntertainmentOperations(result);
}
},
),

View File

@@ -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<ServicesScreen> createState() => _ServicesScreenState();
State<OperationsScreen> createState() => _OperationsScreenState();
}
class _ServicesScreenState extends State<ServicesScreen> {
class _OperationsScreenState extends State<OperationsScreen> {
final ScrollController _scrollController = ScrollController();
@override
@@ -22,12 +22,12 @@ class _ServicesScreenState extends State<ServicesScreen> {
// Agganciamo il listener per la paginazione (Scroll Infinito)
_scrollController.addListener(_onScroll);
// Carichiamo i servizi iniziali
context.read<ServicesCubit>().loadServices();
context.read<OperationsCubit>().loadOperations();
}
void _onScroll() {
if (_isBottom) {
context.read<ServicesCubit>().loadServices();
context.read<OperationsCubit>().loadOperations();
}
}
@@ -60,16 +60,16 @@ class _ServicesScreenState extends State<ServicesScreen> {
),
],
),
body: BlocBuilder<ServicesCubit, ServicesState>(
body: BlocBuilder<OperationsCubit, OperationsState>(
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<ServicesScreen> {
const Text("Nessuna pratica trovata."),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => context.read<ServicesCubit>().loadServices(
refresh: true,
),
onPressed: () => context
.read<OperationsCubit>()
.loadOperations(refresh: true),
child: const Text("Riprova"),
),
],
@@ -90,15 +90,15 @@ class _ServicesScreenState extends State<ServicesScreen> {
// 3. La Lista (con Pull-to-refresh)
return RefreshIndicator(
onRefresh: () =>
context.read<ServicesCubit>().loadServices(refresh: true),
context.read<OperationsCubit>().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<ServicesScreen> {
);
}
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<ServicesScreen> {
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<ServicesScreen> {
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!}
: {},
),
),

View File

@@ -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<SessionCubit>().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<ServicesCubit>().initServiceForm(
existingService: ServiceModel(
context.read<OperationsCubit>().initOperationForm(
existingOperation: OperationModel(
storeId: currentStoreId,
employeeId: member.id,
number: '',