@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
// Importa il tuo SessionCubit e lo State
|
// Importa il tuo SessionCubit e lo State
|
||||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
import 'package:flux/core/data/core_repository.dart';
|
import 'package:flux/core/data/core_repository.dart';
|
||||||
|
import 'package:flux/core/widgets/mobile_upload_screen.dart';
|
||||||
import 'package:flux/features/auth/ui/auth_screen.dart';
|
import 'package:flux/features/auth/ui/auth_screen.dart';
|
||||||
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
|
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
|
||||||
import 'package:flux/features/customers/models/customer_model.dart';
|
import 'package:flux/features/customers/models/customer_model.dart';
|
||||||
@@ -13,8 +14,10 @@ import 'package:flux/features/home/ui/home_screen.dart';
|
|||||||
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
||||||
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
|
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
|
||||||
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
|
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
|
||||||
|
import 'package:flux/features/services/blocs/service_files_bloc.dart';
|
||||||
import 'package:flux/features/services/models/service_model.dart';
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
||||||
|
import 'package:flux/features/services/ui/service_form_screen/service_mobile_upload_screen.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@@ -97,7 +100,23 @@ class AppRouter {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/customer/:id/upload',
|
||||||
|
builder: (context, state) {
|
||||||
|
final customerId = state.pathParameters['id']!;
|
||||||
|
// Recuperiamo il nome dalle query se vogliamo mostrarlo nel titolo,
|
||||||
|
// oppure lo caricherà il bloc.
|
||||||
|
final customerName = state.uri.queryParameters['name'] ?? 'Cliente';
|
||||||
|
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => CustomerFilesBloc(customerId),
|
||||||
|
child: MobileUploadScreen(
|
||||||
|
customerId: customerId,
|
||||||
|
customerName: customerName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/products',
|
path: '/products',
|
||||||
name: 'products',
|
name: 'products',
|
||||||
@@ -118,6 +137,22 @@ class AppRouter {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/service/:id/upload',
|
||||||
|
builder: (context, state) {
|
||||||
|
final serviceId = state.pathParameters['id']!;
|
||||||
|
final serviceName = state.uri.queryParameters['name'] ?? 'Pratica';
|
||||||
|
|
||||||
|
return BlocProvider(
|
||||||
|
// Inizializziamo il bloc col serviceId corretto!
|
||||||
|
create: (context) => ServiceFilesBloc(serviceId: serviceId),
|
||||||
|
child: ServiceMobileUploadScreen(
|
||||||
|
serviceId: serviceId,
|
||||||
|
serviceName: serviceName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class CustomerFilesBloc extends Bloc<CustomerFilesEvent, CustomerFilesState> {
|
|||||||
: super(const CustomerFilesState(status: CustomerFilesStatus.initial)) {
|
: super(const CustomerFilesState(status: CustomerFilesStatus.initial)) {
|
||||||
on<LoadCustomerFilesEvent>(_loadCustomerFiles);
|
on<LoadCustomerFilesEvent>(_loadCustomerFiles);
|
||||||
on<UploadCustomerFileEvent>(_uploadCustomerFile);
|
on<UploadCustomerFileEvent>(_uploadCustomerFile);
|
||||||
on<DeleteCustomerFileEvent>(_deleteCustomerFiles);
|
on<DeleteCustomerFilesEvent>(_deleteCustomerFiles);
|
||||||
on<ToggleCustomerFileSelectionEvent>(_toggleCustomerFileSelection);
|
on<ToggleCustomerFileSelectionEvent>(_toggleCustomerFileSelection);
|
||||||
}
|
}
|
||||||
void _loadCustomerFiles(
|
void _loadCustomerFiles(
|
||||||
@@ -61,7 +61,7 @@ class CustomerFilesBloc extends Bloc<CustomerFilesEvent, CustomerFilesState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _deleteCustomerFiles(
|
Future<void> _deleteCustomerFiles(
|
||||||
DeleteCustomerFileEvent event,
|
DeleteCustomerFilesEvent event,
|
||||||
Emitter<CustomerFilesState> emit,
|
Emitter<CustomerFilesState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: CustomerFilesStatus.loading));
|
emit(state.copyWith(status: CustomerFilesStatus.loading));
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class UploadCustomerFileEvent extends CustomerFilesEvent {
|
|||||||
const UploadCustomerFileEvent({this.pickedFile, this.photo});
|
const UploadCustomerFileEvent({this.pickedFile, this.photo});
|
||||||
}
|
}
|
||||||
|
|
||||||
class DeleteCustomerFileEvent extends CustomerFilesEvent {}
|
class DeleteCustomerFilesEvent extends CustomerFilesEvent {}
|
||||||
|
|
||||||
class ToggleCustomerFileSelectionEvent extends CustomerFilesEvent {
|
class ToggleCustomerFileSelectionEvent extends CustomerFilesEvent {
|
||||||
final CustomerFileModel file;
|
final CustomerFileModel file;
|
||||||
|
|||||||
@@ -107,11 +107,6 @@ class CustomerRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Salva il riferimento del file nel DB
|
|
||||||
Future<void> saveCustomerFile(CustomerFileModel file) async {
|
|
||||||
await _supabase.from('customer_file').insert(file.toMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Carica un file e salva il riferimento nel database
|
/// Carica un file e salva il riferimento nel database
|
||||||
Future<CustomerFileModel> uploadAndRegisterFile({
|
Future<CustomerFileModel> uploadAndRegisterFile({
|
||||||
required String customerId,
|
required String customerId,
|
||||||
|
|||||||
125
lib/features/services/blocs/service_files_bloc.dart
Normal file
125
lib/features/services/blocs/service_files_bloc.dart
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
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/string_extensions.dart';
|
||||||
|
import 'package:flux/features/services/data/services_repository.dart';
|
||||||
|
import 'package:flux/features/services/models/service_file_model.dart';
|
||||||
|
import 'package:flux/features/services/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<DeleteServiceFilesEvent>(_onDeleteServiceFiles);
|
||||||
|
on<ToggleServiceFileSelectionEvent>(_onToggleServiceFileSelection);
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<void> _onServiceSaved(
|
||||||
|
ServiceSavedEvent event,
|
||||||
|
Emitter<ServiceFilesState> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(serviceId: event.serviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<void> _onLoadServiceFiles(
|
||||||
|
LoadServiceFilesEvent event,
|
||||||
|
Emitter<ServiceFilesState> emit,
|
||||||
|
) async {
|
||||||
|
if (serviceId != null) {
|
||||||
|
emit(state.copyWith(status: ServiceFilesStatus.loading));
|
||||||
|
await emit.forEach(
|
||||||
|
_repository.getServiceFilesStream(serviceId!),
|
||||||
|
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 {
|
||||||
|
// BIVIO 1: PRATICA NUOVA (Nessun ID)
|
||||||
|
if (serviceId == 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,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
FutureOr<void> _onDeleteServiceFiles(
|
||||||
|
DeleteServiceFilesEvent event,
|
||||||
|
Emitter<ServiceFilesState> emit,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
FutureOr<void> _onToggleServiceFileSelection(
|
||||||
|
ToggleServiceFileSelectionEvent event,
|
||||||
|
Emitter<ServiceFilesState> emit,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
49
lib/features/services/blocs/service_files_events.dart
Normal file
49
lib/features/services/blocs/service_files_events.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
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? service;
|
||||||
|
const LoadServiceFilesEvent({this.serviceId, this.service});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [serviceId, service];
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddServiceFilesEvent extends ServiceFilesEvent {
|
||||||
|
final List<PlatformFile> files;
|
||||||
|
const AddServiceFilesEvent(this.files);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [files];
|
||||||
|
}
|
||||||
|
|
||||||
|
class UploadServiceFilesEvent extends ServiceFilesEvent {
|
||||||
|
final PlatformFile? pickedFile;
|
||||||
|
final File? photo;
|
||||||
|
const UploadServiceFilesEvent({this.pickedFile, this.photo});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [pickedFile, photo];
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeleteServiceFilesEvent extends ServiceFilesEvent {}
|
||||||
|
|
||||||
|
class ToggleServiceFileSelectionEvent extends ServiceFilesEvent {
|
||||||
|
final ServiceFileModel file;
|
||||||
|
const ToggleServiceFileSelectionEvent(this.file);
|
||||||
|
}
|
||||||
52
lib/features/services/blocs/service_files_state.dart
Normal file
52
lib/features/services/blocs/service_files_state.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import 'package:flux/features/services/models/service_file_model.dart';
|
|||||||
import 'package:flux/features/services/models/service_model.dart';
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
part 'services_state.dart';
|
part 'services_state.dart';
|
||||||
|
|
||||||
class ServicesCubit extends Cubit<ServicesState> {
|
class ServicesCubit extends Cubit<ServicesState> {
|
||||||
@@ -273,49 +274,62 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveAndCopyFileToCustomer(ServiceFileModel file) async {
|
void saveAndCopyFileToCustomer(List<ServiceFileModel> selectedFiles) async {
|
||||||
final currentService = state.currentService;
|
final currentService = state.currentService;
|
||||||
|
|
||||||
|
// 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare
|
||||||
if (currentService == null || currentService.customerId == null) {
|
if (currentService == null || currentService.customerId == null) {
|
||||||
// Magari mostra un errore: non posso copiare al cliente se non c'è un cliente!
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: ServicesStatus.failure,
|
||||||
|
errorMessage:
|
||||||
|
"Impossibile copiare: nessun cliente associato alla pratica.",
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(status: ServicesStatus.loading));
|
emit(state.copyWith(status: ServicesStatus.loading));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Salviamo la pratica (Bozza o definitiva che sia)
|
// 2. SALVATAGGIO CORAZZATO
|
||||||
// Questo assicura che il file sia stato caricato su Storage e censito su DB
|
// Chiamiamo il repo e otteniamo la pratica con TUTTI i file ora dotati di ID e storagePath
|
||||||
await saveCurrentService(isBozza: currentService.isBozza);
|
final updatedService = await _repository.saveFullService(currentService);
|
||||||
|
|
||||||
// 2. Recuperiamo il file "aggiornato"
|
// 3. COPIA RELAZIONALE
|
||||||
// Dopo il saveCurrentService, il file che prima era "locale" ora ha un URL.
|
// Per ogni file che l'utente ha selezionato nella UI, cerchiamo la sua versione
|
||||||
// Lo cerchiamo nella lista aggiornata per nome o estensione.
|
// "ufficiale" (quella con lo storagePath) nel modello appena tornato dal DB.
|
||||||
final savedFile = state.currentService!.files.firstWhere(
|
for (var selectedFile in selectedFiles) {
|
||||||
(f) => f.name == file.name && f.extension == file.extension,
|
// Cerchiamo il match nel modello aggiornato
|
||||||
orElse: () => file,
|
final persistedFile = updatedService.files.firstWhere(
|
||||||
);
|
(f) =>
|
||||||
|
f.name == selectedFile.name &&
|
||||||
|
f.extension == selectedFile.extension,
|
||||||
|
orElse: () => throw Exception(
|
||||||
|
"File ${selectedFile.name} non trovato dopo il salvataggio.",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (savedFile.storagePath.isEmpty) {
|
// Creiamo il link nel database del cliente
|
||||||
throw Exception(
|
await _repository.copyFileToCustomer(
|
||||||
"Errore: URL del file non trovato dopo il salvataggio.",
|
file: persistedFile,
|
||||||
|
customerId: currentService.customerId!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Chiamiamo il repository per la copia fisica nel database del cliente
|
// 4. AGGIORNAMENTO STATO
|
||||||
// Passiamo l'URL del file e l'ID del cliente
|
// Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti"
|
||||||
await _repository.copyFileToCustomer(
|
emit(
|
||||||
file: savedFile,
|
state.copyWith(
|
||||||
customerId: currentService.customerId!,
|
status: ServicesStatus.success,
|
||||||
|
currentService: updatedService,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Feedback all'utente
|
|
||||||
// Potresti emettere un successo o mostrare un toast
|
|
||||||
emit(state.copyWith(status: ServicesStatus.success));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: ServicesStatus.failure,
|
status: ServicesStatus.failure,
|
||||||
errorMessage: "Errore durante la copia del file: $e",
|
errorMessage: "Errore durante il salvataggio e copia: $e",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
|
import 'package:flux/core/utils/string_extensions.dart';
|
||||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||||
import 'package:flux/features/services/models/service_file_model.dart';
|
import 'package:flux/features/services/models/service_file_model.dart';
|
||||||
@@ -83,7 +85,7 @@ class ServicesRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
||||||
Future<void> saveFullService(ServiceModel service) async {
|
Future<ServiceModel> saveFullService(ServiceModel service) async {
|
||||||
try {
|
try {
|
||||||
// 1. Upsert del record principale
|
// 1. Upsert del record principale
|
||||||
final serviceData = await _supabase
|
final serviceData = await _supabase
|
||||||
@@ -150,15 +152,23 @@ class ServicesRepository {
|
|||||||
if (insertTasks.isNotEmpty) {
|
if (insertTasks.isNotEmpty) {
|
||||||
await Future.wait(insertTasks);
|
await Future.wait(insertTasks);
|
||||||
}
|
}
|
||||||
if (service.files.isNotEmpty) {
|
|
||||||
|
// 4. UPLOAD DEI FILE LOCALI (Nuovi)
|
||||||
|
// Filtriamo solo i file che non hanno ancora un ID (quindi sono locali)
|
||||||
|
final localFilesToUpload = service.files
|
||||||
|
.where((f) => f.id == null)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (localFilesToUpload.isNotEmpty) {
|
||||||
final List<Future> uploadTasks = [];
|
final List<Future> uploadTasks = [];
|
||||||
|
|
||||||
for (var file in service.files) {
|
for (var file in localFilesToUpload) {
|
||||||
final storagePath =
|
final storagePath =
|
||||||
'$companyId/services/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}';
|
'$companyId/services/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}';
|
||||||
final String mimeType = file.extension.toLowerCase() == 'pdf'
|
final String mimeType = file.extension.toLowerCase() == 'pdf'
|
||||||
? 'application/pdf'
|
? 'application/pdf'
|
||||||
: 'image/${file.extension}';
|
: 'image/${file.extension}';
|
||||||
|
|
||||||
final fileToSave = file.copyWith(
|
final fileToSave = file.copyWith(
|
||||||
serviceId: newId,
|
serviceId: newId,
|
||||||
storagePath: storagePath,
|
storagePath: storagePath,
|
||||||
@@ -166,22 +176,16 @@ class ServicesRepository {
|
|||||||
|
|
||||||
// Creiamo una funzione asincrona per caricare file e scrivere nel DB
|
// Creiamo una funzione asincrona per caricare file e scrivere nel DB
|
||||||
Future<void> uploadAndLink() async {
|
Future<void> uploadAndLink() async {
|
||||||
// Determiniamo il MIME type corretto in base all'estensione
|
|
||||||
|
|
||||||
// A. Upload nel Bucket Storage (usiamo i bytes così funziona anche su Web!)
|
// A. Upload nel Bucket Storage (usiamo i bytes così funziona anche su Web!)
|
||||||
await _supabase.storage
|
await _supabase.storage
|
||||||
.from('documents')
|
.from('documents')
|
||||||
.uploadBinary(
|
.uploadBinary(
|
||||||
storagePath,
|
storagePath,
|
||||||
fileToSave.localBytes!,
|
fileToSave.localBytes!,
|
||||||
fileOptions: FileOptions(
|
fileOptions: FileOptions(contentType: mimeType, upsert: true),
|
||||||
contentType:
|
|
||||||
mimeType, // Diciamo a Supabase esattamente cos'è!
|
|
||||||
upsert:
|
|
||||||
true, // Opzionale: sovrascrive se esiste già un file con lo stesso nome
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// B. Inserimento riga nel DB relazionale
|
||||||
await _supabase.from('service_file').insert(fileToSave.toMap());
|
await _supabase.from('service_file').insert(fileToSave.toMap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +195,23 @@ class ServicesRepository {
|
|||||||
// Eseguiamo tutti gli upload in parallelo per la massima velocità
|
// Eseguiamo tutti gli upload in parallelo per la massima velocità
|
||||||
await Future.wait(uploadTasks);
|
await Future.wait(uploadTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
.from('service')
|
||||||
|
.select('''
|
||||||
|
*,
|
||||||
|
energy_service(*),
|
||||||
|
fin_service(*),
|
||||||
|
entertainment_service(*),
|
||||||
|
service_file(*)
|
||||||
|
''')
|
||||||
|
.eq('id', newId)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return ServiceModel.fromMap(updatedServiceData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Qui potresti aggiungere una logica di "rollback manuale" se necessario
|
// Qui potresti aggiungere una logica di "rollback manuale" se necessario
|
||||||
throw Exception('Errore durante il salvataggio corazzato: $e');
|
throw Exception('Errore durante il salvataggio corazzato: $e');
|
||||||
@@ -239,12 +260,66 @@ class ServicesRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Ascolta in tempo reale i file caricati per una pratica
|
/// Ascolta in tempo reale i file caricati per una pratica
|
||||||
Stream<List<Map<String, dynamic>>> getServiceFilesStream(String serviceId) {
|
Stream<List<ServiceFileModel>> getServiceFilesStream(String serviceId) {
|
||||||
return _supabase
|
return _supabase
|
||||||
.from('service_file')
|
.from('service_file')
|
||||||
.stream(primaryKey: ['id'])
|
.stream(primaryKey: ['id'])
|
||||||
.eq('service_id', serviceId)
|
.eq('service_id', serviceId)
|
||||||
.order('created_at', ascending: false);
|
.order('created_at', ascending: false)
|
||||||
|
.map(
|
||||||
|
(listOfMaps) =>
|
||||||
|
listOfMaps.map((map) => ServiceFileModel.fromMap(map)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ServiceFileModel> uploadAndRegisterServiceFile({
|
||||||
|
required String serviceId,
|
||||||
|
required PlatformFile pickedFile,
|
||||||
|
}) async {
|
||||||
|
final cleanFileName = pickedFile.name.replaceAll(
|
||||||
|
RegExp(r'[^a-zA-Z0-9\.\-]'),
|
||||||
|
'_',
|
||||||
|
);
|
||||||
|
final storagePath =
|
||||||
|
'$companyId/services/$serviceId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
|
||||||
|
final int fileSize = pickedFile.size;
|
||||||
|
final fileToSave = ServiceFileModel(
|
||||||
|
serviceId: serviceId,
|
||||||
|
name: cleanFileName.fileNameWithoutExtension(),
|
||||||
|
extension: cleanFileName.fileExtension(),
|
||||||
|
storagePath: storagePath,
|
||||||
|
fileSize: fileSize,
|
||||||
|
);
|
||||||
|
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
|
||||||
|
? 'application/pdf'
|
||||||
|
: 'image/${fileToSave.extension}';
|
||||||
|
try {
|
||||||
|
// Usiamo bytes invece del path per massima compatibilità
|
||||||
|
if (pickedFile.bytes == null && pickedFile.path == null) {
|
||||||
|
throw 'Impossibile leggere il contenuto del file';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se siamo su desktop/mobile abbiamo il path, su web abbiamo i bytes
|
||||||
|
if (pickedFile.bytes != null) {
|
||||||
|
await _supabase.storage
|
||||||
|
.from('documents')
|
||||||
|
.uploadBinary(
|
||||||
|
storagePath,
|
||||||
|
pickedFile.bytes!,
|
||||||
|
fileOptions: FileOptions(contentType: mimeType, upsert: true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final response = await _supabase
|
||||||
|
.from('service_file')
|
||||||
|
.insert(fileToSave.toMap())
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return ServiceFileModel.fromMap(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw 'Errore durante l\'upload: $e';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> copyFileToCustomer({
|
Future<void> copyFileToCustomer({
|
||||||
@@ -258,6 +333,6 @@ class ServicesRepository {
|
|||||||
extension: file.extension,
|
extension: file.extension,
|
||||||
fileSize: file.fileSize,
|
fileSize: file.fileSize,
|
||||||
);
|
);
|
||||||
await _customerRepository.saveCustomerFile(fileToCopy);
|
await _customerRepository.saveFileReference(fileToCopy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class ServiceFileModel extends Equatable {
|
|||||||
this.localBytes,
|
this.localBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool get isLocal => localBytes != null;
|
||||||
|
|
||||||
// Trasforma i byte in qualcosa di leggibile (KB, MB, GB)
|
// Trasforma i byte in qualcosa di leggibile (KB, MB, GB)
|
||||||
String get sizeFormatted {
|
String get sizeFormatted {
|
||||||
if (fileSize <= 0) return "0 B";
|
if (fileSize <= 0) return "0 B";
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
import 'package:flux/core/widgets/image_viewer_widget.dart';
|
import 'package:flux/core/widgets/image_viewer_widget.dart';
|
||||||
import 'package:flux/core/widgets/pdf_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/services/blocs/service_files_bloc.dart';
|
||||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
import 'package:flux/features/services/models/service_file_model.dart';
|
import 'package:flux/features/services/models/service_file_model.dart';
|
||||||
|
|
||||||
@@ -25,13 +28,16 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ServicesCubit, ServicesState>(
|
ServiceFilesBloc serviceFilesBloc = BlocProvider.of<ServiceFilesBloc>(
|
||||||
builder: (context, state) {
|
context,
|
||||||
final files = state.currentService?.files ?? [];
|
);
|
||||||
|
|
||||||
|
return BlocBuilder<ServiceFilesBloc, ServiceFilesState>(
|
||||||
|
builder: (context, state) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// --- HEADER SEZIONE ---
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -43,16 +49,38 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
letterSpacing: 1.2,
|
letterSpacing: 1.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
OutlinedButton.icon(
|
Row(
|
||||||
icon: const Icon(Icons.attach_file),
|
children: [
|
||||||
label: const Text("Aggiungi File"),
|
OutlinedButton.icon(
|
||||||
onPressed: () => _pickFiles(context),
|
icon: const Icon(Icons.attach_file),
|
||||||
|
label: const Text("Aggiungi File"),
|
||||||
|
onPressed: () => _pickFiles(context),
|
||||||
|
),
|
||||||
|
if (!context.read<SessionCubit>().state.isMobileDevice) ...[
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => _handleGenerateQr(context),
|
||||||
|
icon: const Icon(Icons.qr_code),
|
||||||
|
label: const Text("GENERA QR"),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withValues(alpha: 0.1),
|
||||||
|
foregroundColor: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary,
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
if (files.isEmpty)
|
// --- LISTA VUOTA ---
|
||||||
|
if (state.allFiles.isEmpty) // Furbata: usiamo allFiles.isEmpty!
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
@@ -70,34 +98,49 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
// --- LISTA PIENA ---
|
||||||
|
else ...[
|
||||||
ListView.builder(
|
ListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
itemCount: files.length,
|
itemCount: state.allFiles.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final file = files[index];
|
final file = state.allFiles[index];
|
||||||
// Calcoliamo la dimensione in MB
|
|
||||||
final sizeMb = (file.fileSize / (1024 * 1024))
|
final sizeMb = (file.fileSize / (1024 * 1024))
|
||||||
.toStringAsFixed(2);
|
.toStringAsFixed(2);
|
||||||
|
|
||||||
// Scegliamo un'icona in base al tipo di file
|
|
||||||
final isPdf = file.extension.toLowerCase() == 'pdf';
|
final isPdf = file.extension.toLowerCase() == 'pdf';
|
||||||
|
final isSelected = state.selectedFiles.contains(file);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => _handleSingleClick(context, file),
|
onTap: () => serviceFilesBloc.add(
|
||||||
|
ToggleServiceFileSelectionEvent(file),
|
||||||
|
),
|
||||||
onDoubleTap: () => _handleDoubleClick(context, file),
|
onDoubleTap: () => _handleDoubleClick(context, file),
|
||||||
child: Card(
|
child: Card(
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
// UX Fina: cambiamo colore del bordo se selezionato
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
side: BorderSide(color: Colors.grey.shade300),
|
side: BorderSide(
|
||||||
|
color: isSelected
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: Colors.grey.shade300,
|
||||||
|
width: isSelected ? 2 : 1,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
// UX Fina: Sfondo leggermente colorato se selezionato
|
||||||
|
color: isSelected
|
||||||
|
? Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withValues(alpha: 0.05)
|
||||||
|
: Theme.of(context).colorScheme.surface,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: Icon(
|
leading: Icon(
|
||||||
isPdf ? Icons.picture_as_pdf : Icons.image,
|
isSelected
|
||||||
color: isPdf ? Colors.red : Colors.blue,
|
? Icons.check_box
|
||||||
|
: Icons.check_box_outline_blank,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -105,35 +148,156 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
subtitle: Text("$sizeMb MB"),
|
subtitle: Text(
|
||||||
trailing: IconButton(
|
file.isLocal ? "$sizeMb MB • (Nuovo)" : "$sizeMb MB",
|
||||||
icon: const Icon(
|
),
|
||||||
Icons.delete_outline,
|
trailing: Icon(
|
||||||
color: Colors.red,
|
isPdf ? Icons.picture_as_pdf : Icons.image,
|
||||||
),
|
color: isPdf ? Colors.red : Colors.blue,
|
||||||
onPressed: () => context
|
size: 32,
|
||||||
.read<ServicesCubit>()
|
|
||||||
.removeAttachment(index),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// --- PANNELLO AZIONI CONTESTUALI (LA MAGIA) ---
|
||||||
|
// Appare SOLO se c'è almeno un file selezionato
|
||||||
|
if (state.selectedFiles.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.primary.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Contatore
|
||||||
|
Text(
|
||||||
|
"${state.selectedFiles.length} file selezionati",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
|
||||||
|
// Bottone Elimina
|
||||||
|
TextButton.icon(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.delete_outline),
|
||||||
|
label: const Text("Elimina"),
|
||||||
|
onPressed: () {
|
||||||
|
// Qui lancerai l'evento per eliminare i file selezionati!
|
||||||
|
// Es: serviceFilesBloc.add(DeleteSelectedFilesEvent());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
|
||||||
|
// Bottone Copia
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.copy),
|
||||||
|
label: const Text("Copia in Cliente"),
|
||||||
|
onPressed: () => saveAndCopyFilesToCustomer(
|
||||||
|
context,
|
||||||
|
state.selectedFiles,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _handleGenerateQr(BuildContext context) async {
|
||||||
|
final cubit = context.read<ServicesCubit>();
|
||||||
|
var currentService = cubit.state.currentService;
|
||||||
|
|
||||||
|
// 1. SE LA PRATICA E' NUOVA (Manca l'ID)
|
||||||
|
if (currentService == null || currentService.id == null) {
|
||||||
|
// Chiediamo conferma
|
||||||
|
final bool? confirm = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text("Salvataggio Necessario"),
|
||||||
|
content: const Text(
|
||||||
|
"Per generare il QR Code e caricare file dal telefono, la pratica deve essere prima salvata in BOZZA.\n\nVuoi salvare ora?",
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, false),
|
||||||
|
child: const Text("Annulla"),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(ctx, true),
|
||||||
|
child: const Text("Salva in Bozza"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirm != true) return; // Utente ha annullato
|
||||||
|
|
||||||
|
// Salviamo forzatamente in bozza
|
||||||
|
await cubit.saveCurrentService(isBozza: true);
|
||||||
|
|
||||||
|
// Recuperiamo il servizio aggiornato con l'ID!
|
||||||
|
currentService = cubit.state.currentService;
|
||||||
|
|
||||||
|
if (currentService?.id == null) {
|
||||||
|
// Se c'è stato un errore nel salvataggio, usciamo
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. ORA ABBIAMO L'ID SICURO -> MOSTRIAMO IL QR!
|
||||||
|
if (context.mounted) {
|
||||||
|
// Creiamo un nome leggibile da passare nel link
|
||||||
|
final nomePratica = "Pratica ${currentService?.customerDisplayName ?? ''}"
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => QrUploadDialog(
|
||||||
|
deepLinkUrl:
|
||||||
|
'fluxapp://service/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}',
|
||||||
|
title: 'Scatta per\n$nomePratica',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- LOGICA DI COPIA AL CLIENTE ---
|
// --- LOGICA DI COPIA AL CLIENTE ---
|
||||||
void _handleSingleClick(BuildContext context, ServiceFileModel file) {
|
void saveAndCopyFilesToCustomer(
|
||||||
|
BuildContext context,
|
||||||
|
List<ServiceFileModel> files,
|
||||||
|
) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: const Text("Copia nei documenti Cliente"),
|
title: const Text("Copia nei documenti Cliente"),
|
||||||
content: const Text(
|
content: const Text(
|
||||||
"Vuoi copiare questo file nell'anagrafica del cliente? \n\n"
|
"Vuoi copiare i file selezionati nell'anagrafica del cliente? \n\n"
|
||||||
"Attenzione: per procedere, la pratica attuale verrà prima salvata in stato BOZZA.",
|
"Attenzione: per procedere, la pratica attuale verrà prima salvata in stato BOZZA.",
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@@ -145,7 +309,7 @@ class AttachmentsSection extends StatelessWidget {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
// 1. Diciamo al Cubit di salvare in Bozza e fare la copia
|
// 1. Diciamo al Cubit di salvare in Bozza e fare la copia
|
||||||
context.read<ServicesCubit>().saveAndCopyFileToCustomer(file);
|
context.read<ServicesCubit>().saveAndCopyFileToCustomer(files);
|
||||||
},
|
},
|
||||||
child: const Text("Salva e Copia"),
|
child: const Text("Salva e Copia"),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
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/services/blocs/service_files_bloc.dart';
|
||||||
|
|
||||||
|
class ServiceMobileUploadScreen extends StatelessWidget {
|
||||||
|
final String serviceId;
|
||||||
|
final String serviceName;
|
||||||
|
|
||||||
|
const ServiceMobileUploadScreen({
|
||||||
|
super.key,
|
||||||
|
required this.serviceId,
|
||||||
|
required this.serviceName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocListener<ServiceFilesBloc, ServiceFilesState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.status == ServiceFilesStatus.success) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text("File caricato! ✅")));
|
||||||
|
}
|
||||||
|
if (state.status == ServiceFilesStatus.failure) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text("Errore: ${state.error}")));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Upload Pratica:\n$serviceName")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 80,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _handleCamera(context),
|
||||||
|
icon: const Icon(Icons.camera_alt_rounded, size: 28),
|
||||||
|
label: const Text(
|
||||||
|
"SCATTA FOTO",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 80,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _handleFilePicker(context),
|
||||||
|
icon: const Icon(Icons.file_present_rounded, size: 28),
|
||||||
|
label: const Text(
|
||||||
|
"CARICA DA MEMORIA",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[200],
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleCamera(BuildContext context) async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final photo = await picker.pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 80,
|
||||||
|
);
|
||||||
|
if (photo != null && context.mounted) {
|
||||||
|
context.read<ServiceFilesBloc>().add(
|
||||||
|
UploadServiceFilesEvent(photo: File(photo.path)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleFilePicker(BuildContext context) async {
|
||||||
|
final result = await FilePicker.pickFiles(withData: true);
|
||||||
|
if (result != null && context.mounted) {
|
||||||
|
context.read<ServiceFilesBloc>().add(
|
||||||
|
UploadServiceFilesEvent(pickedFile: result.files.first),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
pubspec.lock
12
pubspec.lock
@@ -340,10 +340,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: hooks
|
name: hooks
|
||||||
sha256: e79ed1e8e1929bc6ecb6ec85f0cb519c887aa5b423705ded0d0f2d9226def388
|
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.2"
|
version: "1.0.3"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -776,6 +776,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.3"
|
version: "2.7.3"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
retry:
|
retry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
Reference in New Issue
Block a user