@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.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_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ 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/features/attachments/models/attachment_model.dart';
|
||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
part 'customer_files_events.dart';
|
||||
@@ -26,7 +26,7 @@ class CustomerFilesBloc extends Bloc<CustomerFilesEvent, CustomerFilesState> {
|
||||
LoadCustomerFilesEvent event,
|
||||
Emitter<CustomerFilesState> emit,
|
||||
) async {
|
||||
await emit.forEach<List<CustomerFileModel>>(
|
||||
await emit.forEach<List<AttachmentModel>>(
|
||||
_repository.getCustomerFilesStream(customerId),
|
||||
onData: (customerFiles) => CustomerFilesState(
|
||||
status: CustomerFilesStatus.success,
|
||||
@@ -128,7 +128,7 @@ class CustomerFilesBloc extends Bloc<CustomerFilesEvent, CustomerFilesState> {
|
||||
ToggleCustomerFileSelectionEvent event,
|
||||
Emitter<CustomerFilesState> emit,
|
||||
) {
|
||||
List<CustomerFileModel> selectedFiles = List.from(state.selectedFiles);
|
||||
List<AttachmentModel> selectedFiles = List.from(state.selectedFiles);
|
||||
if (selectedFiles.contains(event.file)) {
|
||||
selectedFiles.remove(event.file);
|
||||
} else {
|
||||
|
||||
@@ -25,6 +25,6 @@ class UploadMultipleCustomerFilesEvent extends CustomerFilesEvent {
|
||||
class DeleteCustomerFilesEvent extends CustomerFilesEvent {}
|
||||
|
||||
class ToggleCustomerFileSelectionEvent extends CustomerFilesEvent {
|
||||
final CustomerFileModel file;
|
||||
final AttachmentModel file;
|
||||
const ToggleCustomerFileSelectionEvent(this.file);
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ class CustomerFilesState extends Equatable {
|
||||
|
||||
final CustomerFilesStatus status;
|
||||
final String? error;
|
||||
final List<CustomerFileModel> customerFiles;
|
||||
final List<CustomerFileModel> selectedFiles;
|
||||
final List<AttachmentModel> customerFiles;
|
||||
final List<AttachmentModel> selectedFiles;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, error, customerFiles, selectedFiles];
|
||||
@@ -21,8 +21,8 @@ class CustomerFilesState extends Equatable {
|
||||
CustomerFilesState copyWith({
|
||||
CustomerFilesStatus? status,
|
||||
String? error,
|
||||
List<CustomerFileModel>? customerFiles,
|
||||
List<CustomerFileModel>? selectedFiles,
|
||||
List<AttachmentModel>? customerFiles,
|
||||
List<AttachmentModel>? selectedFiles,
|
||||
}) {
|
||||
return CustomerFilesState(
|
||||
status: status ?? this.status,
|
||||
|
||||
@@ -14,14 +14,12 @@ class CustomerState extends Equatable {
|
||||
final List<CustomerModel> customers;
|
||||
final CustomerModel? lastCreatedCustomer;
|
||||
final String? errorMessage;
|
||||
final List<CustomerFileModel> customerFiles;
|
||||
|
||||
const CustomerState({
|
||||
this.status = CustomerStatus.initial,
|
||||
this.customers = const [],
|
||||
this.lastCreatedCustomer,
|
||||
this.errorMessage,
|
||||
this.customerFiles = const [],
|
||||
});
|
||||
|
||||
CustomerState copyWith({
|
||||
@@ -29,14 +27,12 @@ class CustomerState extends Equatable {
|
||||
List<CustomerModel>? customers,
|
||||
CustomerModel? lastCreatedCustomer,
|
||||
String? errorMessage,
|
||||
List<CustomerFileModel>? customerFiles,
|
||||
}) {
|
||||
return CustomerState(
|
||||
status: status ?? this.status,
|
||||
customers: customers ?? this.customers,
|
||||
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
customerFiles: customerFiles ?? this.customerFiles,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,6 +42,5 @@ class CustomerState extends Equatable {
|
||||
customers,
|
||||
lastCreatedCustomer,
|
||||
errorMessage,
|
||||
customerFiles,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../models/customer_model.dart';
|
||||
@@ -46,11 +45,11 @@ class CustomerRepository {
|
||||
.from('customer')
|
||||
.select('''
|
||||
*,
|
||||
customer_file(*)
|
||||
attachment(*)
|
||||
''')
|
||||
.eq('company_id', companyId)
|
||||
.eq('is_active', true)
|
||||
.order('nome');
|
||||
.order('name');
|
||||
|
||||
return (response as List).map((c) => CustomerModel.fromMap(c)).toList();
|
||||
} catch (e) {
|
||||
@@ -78,36 +77,34 @@ class CustomerRepository {
|
||||
}
|
||||
|
||||
/// Ascolta in tempo reale i file caricati per un cliente
|
||||
Stream<List<CustomerFileModel>> getCustomerFilesStream(String customerId) {
|
||||
Stream<List<AttachmentModel>> getCustomerFilesStream(String customerId) {
|
||||
return _supabase
|
||||
.from('customer_file')
|
||||
.from('attachment')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('customer_id', customerId)
|
||||
.order('created_at', ascending: false)
|
||||
.map(
|
||||
(listOfMaps) =>
|
||||
listOfMaps.map((map) => CustomerFileModel.fromMap(map)).toList(),
|
||||
listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Recupera i file di un cliente specifico
|
||||
Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async {
|
||||
Future<List<AttachmentModel>> getCustomerFiles(String customerId) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('customer_file')
|
||||
.from('attachment')
|
||||
.select()
|
||||
.eq('customer_id', customerId);
|
||||
|
||||
return (response as List)
|
||||
.map((f) => CustomerFileModel.fromMap(f))
|
||||
.toList();
|
||||
return (response as List).map((f) => AttachmentModel.fromMap(f)).toList();
|
||||
} catch (e) {
|
||||
throw '$e';
|
||||
}
|
||||
}
|
||||
|
||||
/// Carica un file e salva il riferimento nel database
|
||||
Future<CustomerFileModel> uploadAndRegisterFile({
|
||||
Future<AttachmentModel> uploadAndRegisterFile({
|
||||
required String customerId,
|
||||
required PlatformFile pickedFile,
|
||||
}) async {
|
||||
@@ -118,7 +115,8 @@ class CustomerRepository {
|
||||
final storagePath =
|
||||
'$companyId/customers/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
|
||||
final int fileSize = pickedFile.size;
|
||||
final fileToSave = CustomerFileModel(
|
||||
final fileToSave = AttachmentModel(
|
||||
companyId: companyId,
|
||||
customerId: customerId,
|
||||
name: cleanFileName.fileNameWithoutExtension(),
|
||||
extension: cleanFileName.fileExtension(),
|
||||
@@ -146,46 +144,47 @@ class CustomerRepository {
|
||||
}
|
||||
|
||||
final response = await _supabase
|
||||
.from('customer_file')
|
||||
.from('attachment')
|
||||
.insert(fileToSave.toMap())
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return CustomerFileModel.fromMap(response);
|
||||
return AttachmentModel.fromMap(response);
|
||||
} catch (e) {
|
||||
throw '$e';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveFileReference(CustomerFileModel file) async {
|
||||
await _supabase.from('customer_file').upsert(file.toMap());
|
||||
Future<void> saveFileReference(AttachmentModel file) async {
|
||||
await _supabase.from('attachment').upsert(file.toMap());
|
||||
}
|
||||
|
||||
/// Aggiorna la lista degli URL nel database
|
||||
Future<void> updateCustomerDocuments(int id, List<String> urls) async {
|
||||
await _supabase
|
||||
.from('customer')
|
||||
.update({'document_urls': urls})
|
||||
.eq('id', id);
|
||||
}
|
||||
|
||||
Future<void> deleteDocuments(List<CustomerFileModel> files) async {
|
||||
Future<void> deleteDocuments(List<AttachmentModel> 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();
|
||||
|
||||
final List<String> idsToDelete = [];
|
||||
final List<String> storagePathsToDelete = [];
|
||||
final List<String> idsToEdit = [];
|
||||
for (var file in files) {
|
||||
if (file.operationId == null) {
|
||||
idsToDelete.add(file.id!);
|
||||
storagePathsToDelete.add(file.storagePath);
|
||||
} else {
|
||||
idsToEdit.add(file.id!);
|
||||
}
|
||||
}
|
||||
try {
|
||||
// 2. Cancellazione MASSIVA dal DB (una sola chiamata invece di un ciclo!)
|
||||
// .in_ dice: "cancella tutti i record il cui ID è contenuto in questa lista"
|
||||
await _supabase
|
||||
.from('customer_file')
|
||||
.delete()
|
||||
.inFilter('id', idsToDelete);
|
||||
|
||||
if (idsToDelete.isNotEmpty) {
|
||||
await _supabase.from('attachment').delete().inFilter('id', idsToDelete);
|
||||
// 3. Cancellazione MASSIVA dallo Storage
|
||||
await _supabase.storage.from('documents').remove(storagePaths);
|
||||
await _supabase.storage.from('documents').remove(storagePathsToDelete);
|
||||
}
|
||||
if (idsToEdit.isNotEmpty) {
|
||||
await _supabase
|
||||
.from('attachment')
|
||||
.update({'customer_id': null})
|
||||
.inFilter('id', idsToEdit);
|
||||
}
|
||||
} on PostgrestException catch (e) {
|
||||
throw e.message;
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class CustomerFileModel extends Equatable {
|
||||
final String? id;
|
||||
final String customerId; // Riferimento UUID
|
||||
final String name;
|
||||
final String storagePath;
|
||||
final String extension;
|
||||
final DateTime? createdAt;
|
||||
final int fileSize;
|
||||
|
||||
const CustomerFileModel({
|
||||
this.id,
|
||||
required this.customerId,
|
||||
required this.name,
|
||||
required this.storagePath,
|
||||
required this.extension,
|
||||
this.createdAt,
|
||||
required this.fileSize,
|
||||
});
|
||||
|
||||
// Trasforma i byte in qualcosa di leggibile (KB, MB, GB)
|
||||
String get sizeFormatted {
|
||||
if (fileSize <= 0) return "0 B";
|
||||
const suffixes = ["B", "KB", "MB", "GB", "TB"];
|
||||
var i = (fileSize.toString().length - 1) ~/ 3;
|
||||
if (i >= suffixes.length) i = suffixes.length - 1;
|
||||
double num = fileSize / (1 << (i * 10));
|
||||
return "${num.toStringAsFixed(i == 0 ? 0 : 1)} ${suffixes[i]}";
|
||||
}
|
||||
|
||||
bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf';
|
||||
|
||||
CustomerFileModel copyWith({
|
||||
String? id,
|
||||
String? customerId,
|
||||
String? name,
|
||||
String? storagePath,
|
||||
String? extension,
|
||||
DateTime? createdAt,
|
||||
int? fileSize,
|
||||
}) {
|
||||
return CustomerFileModel(
|
||||
id: id ?? this.id,
|
||||
customerId: customerId ?? this.customerId,
|
||||
name: name ?? this.name,
|
||||
storagePath: storagePath ?? this.storagePath,
|
||||
extension: extension ?? this.extension,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
fileSize: fileSize ?? this.fileSize,
|
||||
);
|
||||
}
|
||||
|
||||
factory CustomerFileModel.fromMap(Map<String, dynamic> map) {
|
||||
return CustomerFileModel(
|
||||
id: map['id'] as String,
|
||||
customerId: map['customer_id'],
|
||||
name: map['name'],
|
||||
storagePath: map['storage_path'],
|
||||
extension: map['extension'] ?? '',
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
: null,
|
||||
fileSize: map['file_size'] is int
|
||||
? map['file_size']
|
||||
: int.tryParse(map['file_size']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
'customer_id': customerId,
|
||||
'name': name,
|
||||
'storage_path': storagePath,
|
||||
'extension': extension,
|
||||
'file_size': fileSize,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
customerId,
|
||||
name,
|
||||
storagePath,
|
||||
extension,
|
||||
createdAt,
|
||||
fileSize,
|
||||
];
|
||||
}
|
||||
@@ -88,6 +88,11 @@ class CustomerModel extends Equatable {
|
||||
doNotDisturb: map['do_not_disturb'] ?? false,
|
||||
companyId: map['company_id'] as String,
|
||||
isActive: map['is_active'] ?? true,
|
||||
attachments:
|
||||
(map['attachment'] as List?)
|
||||
?.map((x) => AttachmentModel.fromMap(x))
|
||||
.toList() ??
|
||||
const [],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import 'package:flux/core/theme/theme.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/attachments/models/attachment_model.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_file_model.dart';
|
||||
|
||||
class CustomerDetailScreen extends StatefulWidget {
|
||||
final CustomerModel customer;
|
||||
@@ -262,12 +262,12 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
|
||||
|
||||
void _showDeleteConfirmationDialog({
|
||||
required BuildContext context,
|
||||
required List<CustomerFileModel> files,
|
||||
required List<AttachmentModel> files,
|
||||
}) {}
|
||||
}
|
||||
|
||||
class _FileCard extends StatelessWidget {
|
||||
final CustomerFileModel file;
|
||||
final AttachmentModel file;
|
||||
final CustomerFilesState state;
|
||||
const _FileCard({required this.file, required this.state});
|
||||
|
||||
@@ -334,7 +334,7 @@ class _FileCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDoubleClickOnFile(BuildContext context, CustomerFileModel file) {
|
||||
void _handleDoubleClickOnFile(BuildContext context, AttachmentModel file) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
|
||||
@@ -196,11 +196,11 @@ class _CustomerTile extends StatelessWidget {
|
||||
style: TextStyle(color: context.secondaryText),
|
||||
),
|
||||
],
|
||||
if (customer.files.isNotEmpty) ...[
|
||||
if (customer.attachments.isNotEmpty) ...[
|
||||
Text(' - ', style: TextStyle(color: context.secondaryText)),
|
||||
Icon(Icons.attach_file, size: 14, color: context.accent),
|
||||
Text(
|
||||
'${customer.files.length} doc',
|
||||
'${customer.attachments.length} doc',
|
||||
style: TextStyle(
|
||||
color: context.accent,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
||||
@@ -156,7 +156,7 @@ class _LatestOperationsCardContent extends StatelessWidget {
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Text(
|
||||
operation.number,
|
||||
operation.reference,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryText,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
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/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||
import 'package: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:image_picker/image_picker.dart';
|
||||
|
||||
part 'operation_files_events.dart';
|
||||
part 'operation_files_state.dart';
|
||||
@@ -29,9 +28,10 @@ class OperationFilesBloc
|
||||
on<LoadOperationFilesEvent>(_onLoadOperationFiles);
|
||||
on<AddOperationFilesEvent>(_onAddOperationFiles);
|
||||
on<UploadOperationFilesEvent>(_onUploadOperationFiles);
|
||||
on<UploadMultipleOperationFilesEvent>(_onUploadMultipleOperationFiles);
|
||||
on<DeleteOperationFilesEvent>(_onDeleteOperationFiles);
|
||||
on<ToggleOperationFileSelectionEvent>(_onToggleOperationFileSelection);
|
||||
on<LinkFilesToCustomerEvent>(_onLinkFilesToCustomer);
|
||||
|
||||
// Se il BLoC nasce con un ID, accendiamo subito lo stream!
|
||||
if (operationId != null) {
|
||||
add(LoadOperationFilesEvent(operationId: operationId));
|
||||
@@ -41,18 +41,53 @@ class OperationFilesBloc
|
||||
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.
|
||||
) async {
|
||||
// 1. Aggiorniamo l'ID e mettiamo in loading
|
||||
emit(
|
||||
state.copyWith(
|
||||
operationId: event.operationId,
|
||||
localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI
|
||||
status: OperationFilesStatus.uploading,
|
||||
),
|
||||
);
|
||||
|
||||
// Lanciamo il caricamento
|
||||
// 2. RECUPERO E UPLOAD DEI FILE "PARCHEGGIATI" (Pratica Nuova)
|
||||
if (state.localFiles.isNotEmpty) {
|
||||
try {
|
||||
final List<Future<void>> uploadTasks = [];
|
||||
|
||||
for (var file in state.localFiles) {
|
||||
// Ricreiamo il PlatformFile dal nostro AttachmentModel
|
||||
// così il repository lo accetta senza fare storie!
|
||||
final fakePlatformFile = PlatformFile(
|
||||
name: '${file.name}.${file.extension}',
|
||||
size: file.fileSize,
|
||||
bytes: file.localBytes,
|
||||
);
|
||||
|
||||
uploadTasks.add(
|
||||
_repository.uploadAndRegisterOperationFile(
|
||||
operationId: event.operationId, // L'ID APPENA NATO!
|
||||
pickedFile: fakePlatformFile,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Lanciamo tutti gli upload in parallelo
|
||||
await Future.wait(uploadTasks);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationFilesStatus.failure,
|
||||
error: "Errore upload post-salvataggio: $e",
|
||||
),
|
||||
);
|
||||
return; // Ci fermiamo qui se esplode qualcosa
|
||||
}
|
||||
}
|
||||
|
||||
// 3. FINE DEI GIOCHI! Svuotiamo i locali, passiamo a success e accendiamo lo Stream
|
||||
emit(state.copyWith(localFiles: [], status: OperationFilesStatus.success));
|
||||
|
||||
add(LoadOperationFilesEvent(operationId: event.operationId));
|
||||
}
|
||||
|
||||
@@ -60,17 +95,14 @@ class OperationFilesBloc
|
||||
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(
|
||||
_repository.getOperationFilesStream(currentId),
|
||||
onData: (List<AttachmentModel> data) => state.copyWith(
|
||||
status: OperationFilesStatus.success,
|
||||
remoteFiles: data,
|
||||
),
|
||||
@@ -87,13 +119,15 @@ class OperationFilesBloc
|
||||
Emitter<OperationFilesState> emit,
|
||||
) async {
|
||||
final currentId = state.operationId;
|
||||
// BIVIO 1: PRATICA NUOVA (Nessun ID)
|
||||
|
||||
// BIVIO 1: PRATICA NUOVA (Nessun ID - salvataggio locale)
|
||||
if (currentId == null) {
|
||||
// Mettiamo i file nel "parcheggio" locale dello State
|
||||
final companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
|
||||
final newLocalFiles = event.files.map((file) {
|
||||
return OperationFileModel(
|
||||
return AttachmentModel(
|
||||
id: null,
|
||||
operationId: operationId ?? '',
|
||||
companyId: companyId,
|
||||
operationId: '', // Sarà riempito al salvataggio
|
||||
name: file.name.fileNameWithoutExtension(),
|
||||
extension: file.name.fileExtension(),
|
||||
storagePath: '',
|
||||
@@ -101,29 +135,29 @@ class OperationFilesBloc
|
||||
localBytes: file.bytes,
|
||||
);
|
||||
}).toList();
|
||||
final List<OperationFileModel> updatedLocalFiles = [
|
||||
...state.localFiles,
|
||||
...newLocalFiles,
|
||||
];
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
localFiles: updatedLocalFiles,
|
||||
localFiles: [...state.localFiles, ...newLocalFiles],
|
||||
status: OperationFilesStatus.success,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID)
|
||||
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID - Upload immediato)
|
||||
emit(state.copyWith(status: OperationFilesStatus.uploading));
|
||||
try {
|
||||
// Logica identica a quella che abbiamo fatto per i clienti
|
||||
final List<Future<void>> uploadTasks = [];
|
||||
for (var file in event.files) {
|
||||
await _repository.uploadAndRegisterOperationFile(
|
||||
operationId: operationId!,
|
||||
uploadTasks.add(
|
||||
_repository.uploadAndRegisterOperationFile(
|
||||
operationId: currentId,
|
||||
pickedFile: file,
|
||||
),
|
||||
);
|
||||
}
|
||||
await Future.wait(uploadTasks);
|
||||
emit(state.copyWith(status: OperationFilesStatus.success));
|
||||
} catch (e) {
|
||||
emit(
|
||||
@@ -139,51 +173,20 @@ class OperationFilesBloc
|
||||
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",
|
||||
),
|
||||
);
|
||||
if ((event.pickedFiles == null || event.pickedFiles!.isEmpty) &&
|
||||
(event.photos == null || event.photos!.isEmpty)) {
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(status: OperationFilesStatus.uploading, error: null));
|
||||
|
||||
if (state.operationId == null) return;
|
||||
|
||||
emit(state.copyWith(status: OperationFilesStatus.uploading));
|
||||
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!
|
||||
|
||||
// 1. Gestione Documenti normali (PlatformFile)
|
||||
if (event.pickedFiles != null) {
|
||||
for (var file in event.pickedFiles!) {
|
||||
uploadTasks.add(
|
||||
_repository.uploadAndRegisterOperationFile(
|
||||
operationId: state.operationId!,
|
||||
@@ -191,17 +194,40 @@ class OperationFilesBloc
|
||||
),
|
||||
);
|
||||
}
|
||||
// 3. ESECUZIONE PARALLELA!
|
||||
// Aspettiamo che tutti i file siano caricati contemporaneamente.
|
||||
}
|
||||
|
||||
// 2. Gestione Foto Fotocamera (XFile)
|
||||
if (event.photos != null) {
|
||||
for (var photo in event.photos!) {
|
||||
// Leggiamo i byte asincronamente
|
||||
final bytes = await photo.readAsBytes();
|
||||
final fileSize = await photo.length();
|
||||
|
||||
// Lo travestiamo da PlatformFile per passarlo al Repository!
|
||||
final fakePlatformFile = PlatformFile(
|
||||
name: photo.name,
|
||||
size: fileSize,
|
||||
bytes: bytes,
|
||||
path: photo.path,
|
||||
);
|
||||
|
||||
uploadTasks.add(
|
||||
_repository.uploadAndRegisterOperationFile(
|
||||
operationId: state.operationId!,
|
||||
pickedFile: fakePlatformFile,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Esecuzione parallela di tutti i documenti e foto
|
||||
await Future.wait(uploadTasks);
|
||||
// 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",
|
||||
error: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -231,7 +257,7 @@ class OperationFilesBloc
|
||||
ToggleOperationFileSelectionEvent event,
|
||||
Emitter<OperationFilesState> emit,
|
||||
) {
|
||||
List<OperationFileModel> selectedFiles = List.from(state.selectedFiles);
|
||||
final selectedFiles = List<AttachmentModel>.from(state.selectedFiles);
|
||||
if (selectedFiles.contains(event.file)) {
|
||||
selectedFiles.remove(event.file);
|
||||
} else {
|
||||
@@ -239,4 +265,62 @@ class OperationFilesBloc
|
||||
}
|
||||
emit(state.copyWith(selectedFiles: selectedFiles));
|
||||
}
|
||||
|
||||
FutureOr<void> _onLinkFilesToCustomer(
|
||||
LinkFilesToCustomerEvent event,
|
||||
Emitter<OperationFilesState> emit,
|
||||
) async {
|
||||
if (state.selectedFiles.isEmpty) return;
|
||||
|
||||
// BIVIO 1: PRATICA NUOVA (Modalità Locale)
|
||||
if (state.operationId == null) {
|
||||
// Mappiamo i file locali: se sono tra quelli selezionati, iniettiamo il customerId
|
||||
final updatedLocalFiles = state.localFiles.map((file) {
|
||||
if (state.selectedFiles.contains(file)) {
|
||||
return file.copyWith(customerId: event.customerId);
|
||||
}
|
||||
return file;
|
||||
}).toList();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
localFiles: updatedLocalFiles,
|
||||
selectedFiles: [], // Svuotiamo la selezione dopo averli associati
|
||||
status: OperationFilesStatus.success, // o un toast di feedback
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// BIVIO 2: PRATICA ESISTENTE (Modalità Remota su DB)
|
||||
emit(state.copyWith(status: OperationFilesStatus.loading));
|
||||
try {
|
||||
final List<Future<void>> linkTasks = [];
|
||||
|
||||
for (var file in state.selectedFiles) {
|
||||
linkTasks.add(
|
||||
_repository.copyFileToCustomer(
|
||||
file: file,
|
||||
customerId: event.customerId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await Future.wait(linkTasks);
|
||||
|
||||
// Svuotiamo la selezione.
|
||||
// NON serve aggiornare la lista a mano, perché il DB si aggiorna
|
||||
// e lo Stream di Supabase spingerà automaticamente in UI i file aggiornati!
|
||||
emit(
|
||||
state.copyWith(status: OperationFilesStatus.success, selectedFiles: []),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationFilesStatus.failure,
|
||||
error: "Errore associazione: $e",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class OperationsavedEvent extends OperationFilesEvent {
|
||||
|
||||
class LoadOperationFilesEvent extends OperationFilesEvent {
|
||||
final String? operationId;
|
||||
final OperationModel? operation;
|
||||
final AttachmentModel? operation;
|
||||
const LoadOperationFilesEvent({this.operationId, this.operation});
|
||||
|
||||
@override
|
||||
@@ -34,23 +34,25 @@ class AddOperationFilesEvent extends OperationFilesEvent {
|
||||
|
||||
class UploadOperationFilesEvent extends OperationFilesEvent {
|
||||
final List<PlatformFile>? pickedFiles;
|
||||
final List<File>? photos;
|
||||
final List<XFile>? 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);
|
||||
class LinkFilesToCustomerEvent extends OperationFilesEvent {
|
||||
final String customerId;
|
||||
|
||||
const LinkFilesToCustomerEvent({required this.customerId});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [files];
|
||||
List<Object?> get props => [customerId];
|
||||
}
|
||||
|
||||
class DeleteOperationFilesEvent extends OperationFilesEvent {}
|
||||
|
||||
class ToggleOperationFileSelectionEvent extends OperationFilesEvent {
|
||||
final OperationFileModel file;
|
||||
final AttachmentModel file;
|
||||
const ToggleOperationFileSelectionEvent(this.file);
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ class OperationFilesState extends Equatable {
|
||||
final String? operationId;
|
||||
final OperationFilesStatus status;
|
||||
final String? error;
|
||||
final List<OperationFileModel> localFiles;
|
||||
final List<OperationFileModel> remoteFiles;
|
||||
final List<AttachmentModel> localFiles;
|
||||
final List<AttachmentModel> remoteFiles;
|
||||
|
||||
final List<OperationFileModel> selectedFiles;
|
||||
final List<AttachmentModel> selectedFiles;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
@@ -30,15 +30,15 @@ class OperationFilesState extends Equatable {
|
||||
selectedFiles,
|
||||
];
|
||||
|
||||
List<OperationFileModel> get allFiles => [...remoteFiles, ...localFiles];
|
||||
List<AttachmentModel> get allFiles => [...remoteFiles, ...localFiles];
|
||||
|
||||
OperationFilesState copyWith({
|
||||
String? operationId,
|
||||
OperationFilesStatus? status,
|
||||
String? error,
|
||||
List<OperationFileModel>? localFiles,
|
||||
List<OperationFileModel>? remoteFiles,
|
||||
List<OperationFileModel>? selectedFiles,
|
||||
List<AttachmentModel>? localFiles,
|
||||
List<AttachmentModel>? remoteFiles,
|
||||
List<AttachmentModel>? selectedFiles,
|
||||
}) {
|
||||
return OperationFilesState(
|
||||
operationId: operationId ?? this.operationId,
|
||||
|
||||
@@ -4,19 +4,18 @@ 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/attachments/models/attachment_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';
|
||||
import 'package:uuid/uuid.dart';
|
||||
part 'operations_state.dart';
|
||||
|
||||
class OperationsCubit extends Cubit<OperationsState> {
|
||||
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
|
||||
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||
final Uuid _uuid = const Uuid(); // Generatore di UUID per il batch
|
||||
|
||||
OperationsCubit()
|
||||
: super(const OperationsState(status: OperationsStatus.initial));
|
||||
@@ -24,17 +23,13 @@ class OperationsCubit extends Cubit<OperationsState> {
|
||||
// --- CARICAMENTO E PAGINAZIONE ---
|
||||
|
||||
Future<void> loadOperations({bool refresh = false}) async {
|
||||
// Se stiamo già caricando, evitiamo chiamate doppie
|
||||
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: OperationsStatus.loading,
|
||||
errorMessage: null,
|
||||
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
|
||||
allOperations: refresh ? [] : state.allOperations,
|
||||
hasReachedMax: refresh ? false : state.hasReachedMax,
|
||||
),
|
||||
@@ -56,7 +51,6 @@ class OperationsCubit extends Cubit<OperationsState> {
|
||||
dateRange: state.dateRange,
|
||||
);
|
||||
|
||||
// Se ricevi meno record del limite, significa che non ce ne sono altri sul DB
|
||||
final bool reachedMax = newOperations.length < 50;
|
||||
|
||||
emit(
|
||||
@@ -72,7 +66,7 @@ class OperationsCubit extends Cubit<OperationsState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationsStatus.failure,
|
||||
errorMessage: "Errore nel caricamento servizi: $e",
|
||||
errorMessage: "Errore nel caricamento operazioni: $e",
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -80,7 +74,6 @@ class OperationsCubit extends Cubit<OperationsState> {
|
||||
|
||||
// --- GESTIONE FILTRI ---
|
||||
|
||||
/// Aggiorna i parametri di ricerca e ricarica da zero
|
||||
void updateFilters({String? query, DateTimeRange? range}) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -91,15 +84,11 @@ class OperationsCubit extends Cubit<OperationsState> {
|
||||
loadOperations(refresh: true);
|
||||
}
|
||||
|
||||
/// Pulisce tutti i filtri
|
||||
void clearFilters() {
|
||||
emit(state.copyWith(query: '', dateRange: null));
|
||||
loadOperations(refresh: true);
|
||||
}
|
||||
|
||||
// --- GESTIONE BOZZA (DRAFT) ---
|
||||
|
||||
/// Inizializza un nuovo servizio o ne carica uno esistente per la modifica
|
||||
void initOperationForm({
|
||||
OperationModel? existingOperation,
|
||||
String? operationId,
|
||||
@@ -123,14 +112,16 @@ class OperationsCubit extends Cubit<OperationsState> {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Crea un template vuoto con lo store di default (se disponibile)
|
||||
// NUOVA PRATICA: Creiamo un nuovo Batch UUID
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentOperation: OperationModel(
|
||||
storeId: _sessionCubit.state.currentStore?.id ?? '',
|
||||
number: '', // Sarà compilato dall'utente
|
||||
reference: '',
|
||||
createdAt: DateTime.now(),
|
||||
companyId: _sessionCubit.state.company!.id!,
|
||||
status: OperationStatus.draft,
|
||||
batchUuid: _uuid.v4(), // <-- GENERIAMO IL BATCH UNIVOCO
|
||||
),
|
||||
status: OperationsStatus.ready,
|
||||
),
|
||||
@@ -138,68 +129,25 @@ class OperationsCubit extends Cubit<OperationsState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Metodo generico per aggiornare i campi base (AL, MNP, Note, ecc.)
|
||||
void updateField({
|
||||
int? al,
|
||||
int? mnp,
|
||||
int? nip,
|
||||
int? unica,
|
||||
int? telepass,
|
||||
String? note,
|
||||
String? number,
|
||||
bool? isBozza,
|
||||
bool? resultOk,
|
||||
String? customerId,
|
||||
String? customerDisplayName,
|
||||
}) {
|
||||
/// MAGIA PURA: Prepara il form per inserire un altro servizio nella stessa pratica.
|
||||
/// Mantiene il Cliente, il Batch e lo Store, ma svuota il resto.
|
||||
void prepareNextOperationInBatch() {
|
||||
if (state.currentOperation == null) return;
|
||||
|
||||
final updated = state.currentOperation!.copyWith(
|
||||
al: al,
|
||||
mnp: mnp,
|
||||
nip: nip,
|
||||
unica: unica,
|
||||
telepass: telepass,
|
||||
note: note,
|
||||
number: number,
|
||||
isBozza: isBozza,
|
||||
resultOk: resultOk,
|
||||
customerId: customerId,
|
||||
customerDisplayName: customerDisplayName,
|
||||
);
|
||||
final current = state.currentOperation!;
|
||||
|
||||
emit(state.copyWith(currentOperation: updated));
|
||||
}
|
||||
|
||||
// --- GESTIONE MODULI COMPLESSI ---
|
||||
|
||||
void updateEnergyOperations(List<EnergyOperationModel> energyList) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentOperation: state.currentOperation?.copyWith(
|
||||
energyOperations: energyList,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void updateFinOperations(List<FinOperationModel> finList) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentOperation: state.currentOperation?.copyWith(
|
||||
finOperations: finList,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void updateEntertainmentOperations(
|
||||
List<EntertainmentOperationModel> entList,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentOperation: state.currentOperation?.copyWith(
|
||||
entertainmentOperations: entList,
|
||||
status: OperationsStatus.ready,
|
||||
currentOperation: OperationModel(
|
||||
companyId: current.companyId,
|
||||
storeId: current.storeId,
|
||||
storeDisplayName: current.storeDisplayName,
|
||||
batchUuid: current.batchUuid, // <-- MANTIENE IL COLLEGAMENTO
|
||||
customerId: current.customerId, // <-- MANTIENE IL CLIENTE
|
||||
customerDisplayName: current.customerDisplayName,
|
||||
status: OperationStatus.draft,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -208,35 +156,33 @@ class OperationsCubit extends Cubit<OperationsState> {
|
||||
// --- PERSISTENZA ---
|
||||
|
||||
Future<void> saveCurrentOperation({
|
||||
required bool isBozza,
|
||||
required OperationStatus targetStatus,
|
||||
bool shouldPop = true,
|
||||
List<OperationFileModel>? files,
|
||||
}) async {
|
||||
if (state.currentOperation == null) return;
|
||||
|
||||
emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null));
|
||||
try {
|
||||
// 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente
|
||||
final operationToSave = state.currentOperation!.copyWith(
|
||||
isBozza: isBozza,
|
||||
files: files,
|
||||
status: targetStatus,
|
||||
);
|
||||
|
||||
// 2. Salvataggio corazzato
|
||||
final updatedOperation = await _repository.saveFullOperation(
|
||||
operationToSave,
|
||||
);
|
||||
|
||||
// 3. Reset e ricaricamento
|
||||
emit(
|
||||
state.copyWith(
|
||||
// Se non facciamo Pop (es. l'utente vuole aggiungere un altro servizio), non killiamo l'operazione corrente
|
||||
status: shouldPop
|
||||
? OperationsStatus.saved
|
||||
: OperationsStatus.savedNoPop,
|
||||
currentOperation: shouldPop ? null : updatedOperation,
|
||||
),
|
||||
);
|
||||
await loadOperations(refresh: true);
|
||||
|
||||
// Ricarica in background per la dashboard
|
||||
loadOperations(refresh: true);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -247,115 +193,29 @@ class OperationsCubit extends Cubit<OperationsState> {
|
||||
}
|
||||
}
|
||||
|
||||
// --- GESTIONE ALLEGATI LOCALI ---
|
||||
// --- RECUPERO OPERAZIONI DELLO STESSO BATCH (Per UI di riepilogo) ---
|
||||
|
||||
void addAttachments(List<PlatformFile> files) {
|
||||
final newAttachments = files.map((file) {
|
||||
return OperationFileModel(
|
||||
id: null, // Meglio null se non è su DB
|
||||
operationId: state.currentOperation?.id ?? '',
|
||||
name: file.name.fileNameWithoutExtension(),
|
||||
extension: file.name.fileExtension(),
|
||||
storagePath: '',
|
||||
fileSize: file.size,
|
||||
localBytes: file.bytes,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
}).toList();
|
||||
/// Puoi usare questa funzione se nella UI vuoi mostrare "Hai inserito 3 servizi in questa pratica"
|
||||
List<OperationModel> getOperationsInCurrentBatch() {
|
||||
if (state.currentOperation == null) return [];
|
||||
final currentBatch = state.currentOperation!.batchUuid;
|
||||
|
||||
// Creiamo una nuova lista pulita
|
||||
final List<OperationFileModel> updatedList = [
|
||||
...(state.currentOperation?.files ?? []),
|
||||
...newAttachments,
|
||||
];
|
||||
|
||||
// Emettiamo lo stato assicurandoci che il OperationModel venga clonato
|
||||
if (state.currentOperation != null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentOperation: state.currentOperation!.copyWith(
|
||||
files: updatedList,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Filtriamo dalla lista caricata (o potresti fare una query diretta a Supabase se preferisci)
|
||||
return state.allOperations
|
||||
.where(
|
||||
(op) =>
|
||||
op.batchUuid == currentBatch &&
|
||||
op.id != state.currentOperation!.id,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
void removeAttachment(int index) {
|
||||
void updateField({String? customerId, String? customerDisplayName}) {
|
||||
if (state.currentOperation == null) return;
|
||||
|
||||
final updatedList = List<OperationFileModel>.from(
|
||||
state.currentOperation!.files,
|
||||
);
|
||||
updatedList.removeAt(index);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentOperation: state.currentOperation?.copyWith(files: updatedList),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void saveAndCopyFileToCustomer(List<OperationFileModel> selectedFiles) async {
|
||||
final currentOperation = state.currentOperation;
|
||||
|
||||
// 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare
|
||||
if (currentOperation == null || currentOperation.customerId == null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationsStatus.failure,
|
||||
errorMessage:
|
||||
"Impossibile copiare: nessun cliente associato alla pratica.",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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 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 = updatedOperation.files.firstWhere(
|
||||
(f) =>
|
||||
f.name == selectedFile.name &&
|
||||
f.extension == selectedFile.extension,
|
||||
orElse: () => throw Exception(
|
||||
"File ${selectedFile.name} non trovato dopo il salvataggio.",
|
||||
),
|
||||
);
|
||||
|
||||
// Creiamo il link nel database del cliente
|
||||
await _repository.copyFileToCustomer(
|
||||
file: persistedFile,
|
||||
customerId: currentOperation.customerId!,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. AGGIORNAMENTO STATO
|
||||
// Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti"
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationsStatus.success,
|
||||
currentOperation: updatedOperation,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationsStatus.failure,
|
||||
errorMessage: "Errore durante il salvataggio e copia: $e",
|
||||
),
|
||||
final updated = state.currentOperation!.copyWith(
|
||||
customerId: customerId,
|
||||
customerDisplayName: customerDisplayName,
|
||||
);
|
||||
}
|
||||
emit(state.copyWith(currentOperation: updated));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||
import 'package:flux/features/customers/models/customer_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/operation_model.dart';
|
||||
@@ -13,7 +10,6 @@ import '../models/operation_model.dart';
|
||||
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<OperationModel> fetchOperationById(String id) async {
|
||||
@@ -23,7 +19,11 @@ class OperationsRepository {
|
||||
.select('''
|
||||
*,
|
||||
customer(name),
|
||||
staff_member(name)
|
||||
store(name),
|
||||
staff_member(name),
|
||||
provider(name),
|
||||
model(name_with_brand),
|
||||
attachments(*)
|
||||
''')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
@@ -48,6 +48,9 @@ class OperationsRepository {
|
||||
.select('''
|
||||
*,
|
||||
customer(name),
|
||||
store(name),
|
||||
provider(name),
|
||||
model(name_with_brand),
|
||||
staff_member(name),
|
||||
attachments(*)
|
||||
''')
|
||||
@@ -107,49 +110,6 @@ class OperationsRepository {
|
||||
|
||||
final String newId = operationData['id'];
|
||||
|
||||
// 4. UPLOAD DEI FILE LOCALI (Nuovi)
|
||||
// Filtriamo solo i file che non hanno ancora un ID (quindi sono locali)
|
||||
final localFilesToUpload = operation.attachments
|
||||
.where((f) => f.id == null)
|
||||
.toList();
|
||||
|
||||
if (localFilesToUpload.isNotEmpty) {
|
||||
final List<Future> uploadTasks = [];
|
||||
|
||||
for (var file in localFilesToUpload) {
|
||||
final storagePath =
|
||||
'$companyId/operations/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}';
|
||||
final String mimeType = file.extension.toLowerCase() == 'pdf'
|
||||
? 'application/pdf'
|
||||
: 'image/${file.extension}';
|
||||
|
||||
final fileToSave = file.copyWith(
|
||||
operationId: newId,
|
||||
storagePath: storagePath,
|
||||
);
|
||||
|
||||
// Creiamo una funzione asincrona per caricare file e scrivere nel DB
|
||||
Future<void> uploadAndLink() async {
|
||||
// A. Upload nel Bucket Storage (usiamo i bytes così funziona anche su Web!)
|
||||
await _supabase.storage
|
||||
.from('documents')
|
||||
.uploadBinary(
|
||||
storagePath,
|
||||
fileToSave.localBytes!,
|
||||
fileOptions: FileOptions(contentType: mimeType, upsert: true),
|
||||
);
|
||||
|
||||
// B. Inserimento riga nel DB relazionale
|
||||
await _supabase.from('attachment').insert(fileToSave.toMap());
|
||||
}
|
||||
|
||||
uploadTasks.add(uploadAndLink());
|
||||
}
|
||||
|
||||
// Eseguiamo tutti gli upload in parallelo per la massima velocità
|
||||
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 operation_file appena inseriti)
|
||||
@@ -158,6 +118,9 @@ class OperationsRepository {
|
||||
.select('''
|
||||
*,
|
||||
staff_member(name),
|
||||
store(name),
|
||||
provider(name),
|
||||
model(name_with_brand),
|
||||
customer(name),
|
||||
attachments(*)
|
||||
''')
|
||||
@@ -278,7 +241,7 @@ class OperationsRepository {
|
||||
}
|
||||
|
||||
Future<void> copyFileToCustomer({
|
||||
required OperationFileModel file,
|
||||
required AttachmentModel file,
|
||||
required String customerId,
|
||||
}) async {
|
||||
await _supabase
|
||||
@@ -290,16 +253,28 @@ class OperationsRepository {
|
||||
Future<void> deleteOperationFiles(List<AttachmentModel> 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();
|
||||
|
||||
final List<String> idsToDelete = [];
|
||||
final List<String> idsToEdit = [];
|
||||
final List<String> storagePathsToDelete = [];
|
||||
for (var file in files) {
|
||||
if (file.customerId == null) {
|
||||
idsToDelete.add(file.id!);
|
||||
storagePathsToDelete.add(file.storagePath);
|
||||
} else {
|
||||
idsToEdit.add(file.id!);
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (idsToDelete.isNotEmpty) {
|
||||
await _supabase.from('attachment').delete().inFilter('id', idsToDelete);
|
||||
await _supabase.storage.from('documents').remove(storagePathsToDelete);
|
||||
}
|
||||
if (idsToEdit.isNotEmpty) {
|
||||
await _supabase
|
||||
.from('attachment')
|
||||
.update({'operation_id': null})
|
||||
.inFilter('id', idsToDelete);
|
||||
|
||||
await _supabase.storage.from('documents').remove(storagePaths);
|
||||
.inFilter('id', idsToEdit);
|
||||
}
|
||||
} on PostgrestException catch (e) {
|
||||
throw 'Errore database: ${e.message}';
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class OperationFileModel extends Equatable {
|
||||
final String? id;
|
||||
final DateTime? createdAt;
|
||||
final String name;
|
||||
final String extension;
|
||||
final String storagePath;
|
||||
final String operationId;
|
||||
final int fileSize;
|
||||
final Uint8List? localBytes;
|
||||
|
||||
const OperationFileModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
required this.name,
|
||||
required this.extension,
|
||||
required this.storagePath,
|
||||
required this.operationId,
|
||||
required this.fileSize,
|
||||
this.localBytes,
|
||||
});
|
||||
|
||||
bool get isLocal => localBytes != null;
|
||||
|
||||
// Trasforma i byte in qualcosa di leggibile (KB, MB, GB)
|
||||
String get sizeFormatted {
|
||||
if (fileSize <= 0) return "0 B";
|
||||
const suffixes = ["B", "KB", "MB", "GB", "TB"];
|
||||
var i = (fileSize.toString().length - 1) ~/ 3;
|
||||
if (i >= suffixes.length) i = suffixes.length - 1;
|
||||
double num = fileSize / (1 << (i * 10));
|
||||
return "${num.toStringAsFixed(i == 0 ? 0 : 1)} ${suffixes[i]}";
|
||||
}
|
||||
|
||||
bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf';
|
||||
|
||||
OperationFileModel copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
String? name,
|
||||
String? extension,
|
||||
String? storagePath,
|
||||
String? operationId,
|
||||
int? fileSize,
|
||||
Uint8List? localBytes,
|
||||
}) {
|
||||
return OperationFileModel(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
name: name ?? this.name,
|
||||
extension: extension ?? this.extension,
|
||||
storagePath: storagePath ?? this.storagePath,
|
||||
operationId: operationId ?? this.operationId,
|
||||
fileSize: fileSize ?? this.fileSize,
|
||||
localBytes: localBytes ?? this.localBytes,
|
||||
);
|
||||
}
|
||||
|
||||
factory OperationFileModel.fromMap(Map<String, dynamic> map) {
|
||||
return OperationFileModel(
|
||||
id: map['id'] as String,
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
: null,
|
||||
name: map['name'] ?? '',
|
||||
extension: map['extension'] ?? '',
|
||||
storagePath: map['storage_path'] ?? '',
|
||||
operationId: map['operation_id']?.toString() ?? '',
|
||||
fileSize: map['file_size'] is int
|
||||
? map['file_size']
|
||||
: int.tryParse(map['file_size']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
'name': name,
|
||||
'extension': extension,
|
||||
'storage_path': storagePath,
|
||||
'operation_id': operationId,
|
||||
'file_size': fileSize,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
createdAt,
|
||||
name,
|
||||
extension,
|
||||
storagePath,
|
||||
operationId,
|
||||
fileSize,
|
||||
localBytes,
|
||||
];
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||
|
||||
enum OperationStatus {
|
||||
@@ -27,7 +28,9 @@ class OperationModel extends Equatable {
|
||||
final DateTime? createdAt;
|
||||
final String type;
|
||||
final String? providerId;
|
||||
final String? providerDisplayName;
|
||||
final String? modelId;
|
||||
final String? modelDisplayName;
|
||||
final String? description;
|
||||
final DateTime? expirationDate;
|
||||
final String note;
|
||||
@@ -35,13 +38,14 @@ class OperationModel extends Equatable {
|
||||
final String batchUuid;
|
||||
final String companyId;
|
||||
final String storeId;
|
||||
final String? storeDisplayName;
|
||||
final int quantity;
|
||||
final String? staffId;
|
||||
final String staffDisplayName;
|
||||
final String? staffDisplayName;
|
||||
final String? lastCampaignId;
|
||||
final OperationStatus status;
|
||||
final String? customerId;
|
||||
final String customerDisplayName;
|
||||
final String? customerDisplayName;
|
||||
final String reference;
|
||||
|
||||
// ALLEGATI (Aggiunto)
|
||||
@@ -52,7 +56,9 @@ class OperationModel extends Equatable {
|
||||
this.createdAt,
|
||||
this.type = '',
|
||||
this.providerId,
|
||||
this.providerDisplayName,
|
||||
this.modelId,
|
||||
this.modelDisplayName,
|
||||
this.description,
|
||||
this.expirationDate,
|
||||
this.note = '',
|
||||
@@ -60,13 +66,14 @@ class OperationModel extends Equatable {
|
||||
this.batchUuid = '',
|
||||
required this.companyId,
|
||||
this.storeId = '',
|
||||
this.storeDisplayName,
|
||||
this.quantity = 1,
|
||||
this.staffId,
|
||||
this.staffDisplayName = '',
|
||||
this.staffDisplayName,
|
||||
this.lastCampaignId,
|
||||
this.status = OperationStatus.draft,
|
||||
this.customerId,
|
||||
this.customerDisplayName = '',
|
||||
this.customerDisplayName,
|
||||
this.reference = '',
|
||||
this.attachments = const [],
|
||||
});
|
||||
@@ -76,7 +83,9 @@ class OperationModel extends Equatable {
|
||||
DateTime? createdAt,
|
||||
String? type,
|
||||
String? providerId,
|
||||
String? providerDisplayName,
|
||||
String? modelId,
|
||||
String? modelDisplayName,
|
||||
String? description,
|
||||
DateTime? expirationDate,
|
||||
String? note,
|
||||
@@ -84,6 +93,7 @@ class OperationModel extends Equatable {
|
||||
String? batchUuid,
|
||||
String? companyId,
|
||||
String? storeId,
|
||||
String? storeDisplayName,
|
||||
int? quantity,
|
||||
String? staffId,
|
||||
String? staffDisplayName,
|
||||
@@ -98,7 +108,9 @@ class OperationModel extends Equatable {
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
type: type ?? this.type,
|
||||
providerId: providerId ?? this.providerId,
|
||||
providerDisplayName: providerDisplayName ?? this.providerDisplayName,
|
||||
modelId: modelId ?? this.modelId,
|
||||
modelDisplayName: modelDisplayName ?? this.modelDisplayName,
|
||||
description: description ?? this.description,
|
||||
expirationDate: expirationDate ?? this.expirationDate,
|
||||
note: note ?? this.note,
|
||||
@@ -106,6 +118,7 @@ class OperationModel extends Equatable {
|
||||
batchUuid: batchUuid ?? this.batchUuid,
|
||||
companyId: companyId ?? this.companyId,
|
||||
storeId: storeId ?? this.storeId,
|
||||
storeDisplayName: storeDisplayName ?? this.storeDisplayName,
|
||||
quantity: quantity ?? this.quantity,
|
||||
staffId: staffId ?? this.staffId,
|
||||
staffDisplayName: staffDisplayName ?? this.staffDisplayName,
|
||||
@@ -123,7 +136,9 @@ class OperationModel extends Equatable {
|
||||
createdAt,
|
||||
type,
|
||||
providerId,
|
||||
providerDisplayName,
|
||||
modelId,
|
||||
modelDisplayName,
|
||||
description,
|
||||
expirationDate,
|
||||
note,
|
||||
@@ -131,6 +146,7 @@ class OperationModel extends Equatable {
|
||||
batchUuid,
|
||||
companyId,
|
||||
storeId,
|
||||
storeDisplayName,
|
||||
quantity,
|
||||
staffId,
|
||||
staffDisplayName,
|
||||
@@ -154,7 +170,9 @@ class OperationModel extends Equatable {
|
||||
: null,
|
||||
type: map['type'] as String? ?? '',
|
||||
providerId: map['provider_id'] as String? ?? '',
|
||||
providerDisplayName: "${map['provider']['name']}".myFormat(),
|
||||
modelId: map['model_id'] as String? ?? '',
|
||||
modelDisplayName: "${map['model']['name_with_brand'] ?? ''}".myFormat(),
|
||||
description: map['description'] as String? ?? '',
|
||||
expirationDate: map['expiration_date'] != null
|
||||
? DateTime.parse(map['expiration_date'])
|
||||
@@ -164,13 +182,22 @@ class OperationModel extends Equatable {
|
||||
batchUuid: map['batch_uuid'] as String,
|
||||
companyId: map['company_id'] as String,
|
||||
storeId: map['store_id'] as String? ?? '',
|
||||
storeDisplayName: "${map['store']['name']}".myFormat(),
|
||||
quantity: map['quantity'] is int
|
||||
? map['quantity']
|
||||
: int.tryParse(map['quantity']?.toString() ?? '0') ?? 0,
|
||||
staffId: map['staff_id'] as String? ?? '',
|
||||
staffDisplayName: "${map['staff_member']['name'] ?? ''}".myFormat(),
|
||||
lastCampaignId: map['last_campaign_id'] as String? ?? '',
|
||||
status: OperationStatus.fromString(map['status']),
|
||||
customerId: map['customer_id'] as String? ?? '',
|
||||
customerDisplayName: "${map['customer']['name'] ?? ''}".myFormat(),
|
||||
attachments:
|
||||
(map['attachment'] as List?)
|
||||
?.map((x) => AttachmentModel.fromMap(x))
|
||||
.toList() ??
|
||||
const [],
|
||||
|
||||
reference: map['reference'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/utils/extensions.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/attachments/models/attachment_model.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/operation_file_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
|
||||
class AttachmentsSection extends StatelessWidget {
|
||||
const AttachmentsSection({super.key});
|
||||
@@ -227,11 +229,31 @@ class AttachmentsSection extends StatelessWidget {
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
label: const Text("Copia in Cliente"),
|
||||
onPressed: () => saveAndCopyFilesToCustomer(
|
||||
context,
|
||||
state.selectedFiles,
|
||||
onPressed: () {
|
||||
final cubit = context.read<OperationsCubit>();
|
||||
if (cubit.state.currentOperation?.customerId ==
|
||||
null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context
|
||||
.l10n
|
||||
.operationFormAttachmentSectionNoCustomer,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
context.read<OperationFilesBloc>().add(
|
||||
LinkFilesToCustomerEvent(
|
||||
customerId: cubit
|
||||
.state
|
||||
.currentOperation!
|
||||
.customerId!,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -278,9 +300,8 @@ class AttachmentsSection extends StatelessWidget {
|
||||
|
||||
// Salviamo forzatamente in bozza
|
||||
await cubit.saveCurrentOperation(
|
||||
isBozza: true,
|
||||
targetStatus: OperationStatus.draft,
|
||||
shouldPop: false,
|
||||
files: operationFilesBloc.state.localFiles,
|
||||
);
|
||||
|
||||
// Recuperiamo il servizio aggiornato con l'ID!
|
||||
@@ -321,39 +342,8 @@ class AttachmentsSection extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// --- LOGICA DI COPIA AL CLIENTE ---
|
||||
void saveAndCopyFilesToCustomer(
|
||||
BuildContext context,
|
||||
List<OperationFileModel> files,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text("Copia nei documenti Cliente"),
|
||||
content: const Text(
|
||||
"Vuoi copiare i file selezionati nell'anagrafica del cliente? \n\n"
|
||||
"Attenzione: per procedere, la pratica attuale verrà prima salvata in stato BOZZA.",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx),
|
||||
child: const Text("Annulla"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
// 1. Diciamo al Cubit di salvare in Bozza e fare la copia
|
||||
context.read<OperationsCubit>().saveAndCopyFileToCustomer(files);
|
||||
},
|
||||
child: const Text("Salva e Copia"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// --- LOGICA DI VISUALIZZAZIONE OVERLAY ---
|
||||
void _handleDoubleClick(BuildContext context, OperationFileModel file) {
|
||||
void _handleDoubleClick(BuildContext context, AttachmentModel file) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
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/operation_model.dart';
|
||||
|
||||
class GeneralInfoSection extends StatelessWidget {
|
||||
@@ -34,7 +32,7 @@ class GeneralInfoSection extends StatelessWidget {
|
||||
|
||||
// Numero di Riferimento / Telefono
|
||||
TextFormField(
|
||||
initialValue: operation.number,
|
||||
initialValue: operation.reference,
|
||||
keyboardType: TextInputType
|
||||
.phone, // Fa aprire il tastierino numerico su mobile
|
||||
decoration: const InputDecoration(
|
||||
@@ -43,49 +41,6 @@ class GeneralInfoSection extends StatelessWidget {
|
||||
border: OutlineInputBorder(),
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
),
|
||||
onChanged: (val) {
|
||||
context.read<OperationsCubit>().updateField(number: val);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// I due Switch affiancati (Bozza e A buon fine)
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SwitchListTile(
|
||||
title: const Text("Bozza"),
|
||||
subtitle: const Text(
|
||||
"Pratica in lavorazione",
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
value: operation.isBozza,
|
||||
activeThumbColor: Colors.orange,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onChanged: (val) {
|
||||
context.read<OperationsCubit>().updateField(isBozza: val);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: SwitchListTile(
|
||||
title: const Text("A buon fine"),
|
||||
subtitle: const Text(
|
||||
"Esito positivo",
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
value: operation.resultOk,
|
||||
activeThumbColor: Colors.green,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onChanged: (val) {
|
||||
context.read<OperationsCubit>().updateField(
|
||||
resultOk: val,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -101,9 +56,6 @@ class GeneralInfoSection extends StatelessWidget {
|
||||
border: OutlineInputBorder(),
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
onChanged: (val) {
|
||||
context.read<OperationsCubit>().updateField(note: val);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 OperationFormScreen extends StatefulWidget {
|
||||
final String? operationId;
|
||||
@@ -34,9 +33,16 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
void _performSave(BuildContext context, {required bool isBozza}) {
|
||||
void _performSave(
|
||||
BuildContext context, {
|
||||
required OperationStatus targetStatus,
|
||||
required bool shouldPop,
|
||||
}) {
|
||||
FocusScope.of(context).unfocus();
|
||||
context.read<OperationsCubit>().saveCurrentOperation(isBozza: isBozza);
|
||||
context.read<OperationsCubit>().saveCurrentOperation(
|
||||
targetStatus: targetStatus,
|
||||
shouldPop: shouldPop,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -93,7 +99,11 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit_note),
|
||||
tooltip: "Salva come Bozza",
|
||||
onPressed: () => _performSave(context, isBozza: true),
|
||||
onPressed: () => _performSave(
|
||||
context,
|
||||
targetStatus: OperationStatus.draft,
|
||||
shouldPop: false,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
@@ -101,7 +111,11 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
color: Colors.green,
|
||||
),
|
||||
tooltip: "Conferma Pratica",
|
||||
onPressed: () => _performSave(context, isBozza: false),
|
||||
onPressed: () => _performSave(
|
||||
context,
|
||||
targetStatus: OperationStatus.ok,
|
||||
shouldPop: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
@@ -120,9 +134,6 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
GeneralInfoSection(operation: operation),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
OperationsGrid(operation: operation),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
AttachmentsSection(),
|
||||
const SizedBox(height: 32),
|
||||
_buildBottomActionButtons(context, isSaving: isSaving),
|
||||
@@ -152,7 +163,11 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
label: const Text("Salva in Bozza"),
|
||||
onPressed: isSaving
|
||||
? null
|
||||
: () => _performSave(context, isBozza: true),
|
||||
: () => _performSave(
|
||||
context,
|
||||
targetStatus: OperationStatus.draft,
|
||||
shouldPop: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -173,7 +188,11 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
),
|
||||
onPressed: isSaving
|
||||
? null
|
||||
: () => _performSave(context, isBozza: false),
|
||||
: () => _performSave(
|
||||
context,
|
||||
targetStatus: OperationStatus.ok,
|
||||
shouldPop: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -296,7 +296,7 @@ class _OperationMobileUploadScreenState
|
||||
// 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<OperationFilesBloc>();
|
||||
bloc.add(UploadMultipleOperationFilesEvent(_stagedFiles));
|
||||
bloc.add(UploadOperationFilesEvent(pickedFiles: _stagedFiles));
|
||||
|
||||
// N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"!
|
||||
}
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
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_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 OperationsGrid extends StatelessWidget {
|
||||
final OperationModel operation;
|
||||
|
||||
const OperationsGrid({super.key, required this.operation});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.layers_outlined,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Servizi e Accessori",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 16,
|
||||
alignment: WrapAlignment.center,
|
||||
children: [
|
||||
// --- CONTATORI SEMPLICI ---
|
||||
ActionCard(
|
||||
label: "AL",
|
||||
count: operation.al,
|
||||
icon: Icons.sim_card,
|
||||
color: Colors.blue,
|
||||
onTap: () => updateCountDialog(
|
||||
context,
|
||||
"AL",
|
||||
operation.al,
|
||||
(val) =>
|
||||
context.read<OperationsCubit>().updateField(al: val),
|
||||
),
|
||||
),
|
||||
ActionCard(
|
||||
label: "MNP",
|
||||
count: operation.mnp,
|
||||
icon: Icons.phone_android,
|
||||
color: Colors.indigo,
|
||||
onTap: () => updateCountDialog(
|
||||
context,
|
||||
"MNP",
|
||||
operation.mnp,
|
||||
(val) =>
|
||||
context.read<OperationsCubit>().updateField(mnp: val),
|
||||
),
|
||||
),
|
||||
ActionCard(
|
||||
label: "NIP",
|
||||
count: operation.nip,
|
||||
icon: Icons.compare_arrows,
|
||||
color: Colors.cyan,
|
||||
onTap: () => updateCountDialog(
|
||||
context,
|
||||
"NIP",
|
||||
operation.nip,
|
||||
(val) =>
|
||||
context.read<OperationsCubit>().updateField(nip: val),
|
||||
),
|
||||
),
|
||||
ActionCard(
|
||||
label: "Unica",
|
||||
count: operation.unica,
|
||||
icon: Icons.all_inclusive,
|
||||
color: Colors.purple,
|
||||
onTap: () => updateCountDialog(
|
||||
context,
|
||||
"Unica",
|
||||
operation.unica,
|
||||
(val) => context.read<OperationsCubit>().updateField(
|
||||
unica: val,
|
||||
),
|
||||
),
|
||||
),
|
||||
ActionCard(
|
||||
label: "Telepass",
|
||||
count: operation.telepass,
|
||||
icon: Icons.directions_car,
|
||||
color: Colors.amber.shade700,
|
||||
onTap: () => updateCountDialog(
|
||||
context,
|
||||
"Telepass",
|
||||
operation.telepass,
|
||||
(val) => context.read<OperationsCubit>().updateField(
|
||||
telepass: val,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// --- MODULI COMPLESSI (Le liste) ---
|
||||
ActionCard(
|
||||
label: "Energia",
|
||||
count: operation.energyOperations.length,
|
||||
icon: Icons.bolt,
|
||||
color: Colors.green,
|
||||
onTap: () async {
|
||||
// Apriamo la modale e aspettiamo il risultato
|
||||
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<OperationsCubit>().updateEnergyOperations(
|
||||
result,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ActionCard(
|
||||
label: "Finanziam.",
|
||||
count: operation.finOperations.length,
|
||||
icon: Icons.euro_symbol,
|
||||
color: Colors.teal,
|
||||
onTap: () async {
|
||||
final result = await showDialog<List<FinOperationModel>>(
|
||||
context: context,
|
||||
builder: (context) => FinanceOperationDialog(
|
||||
productCubit: context.read<ProductCubit>(),
|
||||
currentStoreId: operation.storeId,
|
||||
initialOperations: operation
|
||||
.finOperations, // Passiamo la lista attuale
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
context.read<OperationsCubit>().updateFinOperations(
|
||||
result,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ActionCard(
|
||||
label: "Intratten.",
|
||||
count: operation.entertainmentOperations.length,
|
||||
icon: Icons.movie_filter_outlined,
|
||||
color: Colors.purple,
|
||||
onTap: () async {
|
||||
final result =
|
||||
await showDialog<List<EntertainmentOperationModel>>(
|
||||
context: context,
|
||||
builder: (context) => EntertainmentOperationDialog(
|
||||
initialOperations:
|
||||
operation.entertainmentOperations,
|
||||
currentStoreId: operation.storeId,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
context
|
||||
.read<OperationsCubit>()
|
||||
.updateEntertainmentOperations(result);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ 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/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
|
||||
|
||||
@@ -139,15 +138,6 @@ class _OperationsScreenState extends State<OperationsScreen> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (operation.isBozza)
|
||||
const Chip(
|
||||
label: Text(
|
||||
"BOZZA",
|
||||
style: TextStyle(fontSize: 10, color: Colors.white),
|
||||
),
|
||||
backgroundColor: Colors.orange,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: Column(
|
||||
@@ -155,21 +145,14 @@ class _OperationsScreenState extends State<OperationsScreen> {
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"Pratica: ${operation.number} • ${operation.createdAt?.day}/${operation.createdAt?.month}/${operation.createdAt?.year}",
|
||||
"Pratica: ${operation.reference} • ${operation.createdAt?.day}/${operation.createdAt?.month}/${operation.createdAt?.year}",
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// I nostri mini-chip per i servizi attivati
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
Row(
|
||||
children: [
|
||||
if (operation.al > 0 || operation.mnp > 0)
|
||||
_miniBadge("📞 Tel", Colors.blue),
|
||||
if (operation.energyOperations.isNotEmpty)
|
||||
_miniBadge("⚡ Energy", Colors.green),
|
||||
if (operation.finOperations.isNotEmpty)
|
||||
_miniBadge("💰 Fin", Colors.purple),
|
||||
if (operation.entertainmentOperations.isNotEmpty)
|
||||
_miniBadge("📺 Ent", Colors.red),
|
||||
Text(operation.type),
|
||||
const SizedBox(width: 8),
|
||||
_buildOperationStatus(operation.status),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -187,22 +170,31 @@ class _OperationsScreenState extends State<OperationsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _miniBadge(String text, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: color.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Widget _buildOperationStatus(OperationStatus status) {
|
||||
Color color;
|
||||
switch (status) {
|
||||
case OperationStatus.canceled || OperationStatus.ko:
|
||||
color = Colors.grey.shade800;
|
||||
break;
|
||||
case OperationStatus.waitingforaction || OperationStatus.draft:
|
||||
color = Colors.orange;
|
||||
break;
|
||||
case OperationStatus.ok:
|
||||
color = Colors.green;
|
||||
break;
|
||||
case OperationStatus.waitingfordeployment ||
|
||||
OperationStatus.waitingforsupport:
|
||||
color = Colors.blue;
|
||||
break;
|
||||
}
|
||||
return Chip(
|
||||
label: Text("BOZZA", style: TextStyle(fontSize: 10, color: Colors.white)),
|
||||
backgroundColor: color,
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
}
|
||||
|
||||
void startNewOperation(BuildContext context) {
|
||||
context.pushNamed('operation-form');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
|
||||
import 'package:flux/features/operations/blocs/operations_cubit.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 startNewOperation(BuildContext context) {
|
||||
final session = context.read<SessionCubit>().state;
|
||||
final currentStoreId = session.currentStore?.id;
|
||||
|
||||
if (currentStoreId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text("Seleziona uno store prima di iniziare")),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (modalContext) {
|
||||
// Usiamo lo StoreCubit invece dello StaffCubit!
|
||||
return BlocBuilder<StoreCubit, StoreState>(
|
||||
builder: (context, storeState) {
|
||||
// Recuperiamo lo staff assegnato a questo specifico store usando la mappa che avevi già creato
|
||||
final storeStaff = storeState.staffByStore[currentStoreId] ?? [];
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
"Chi sta eseguendo l'operazione?",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
if (storeStaff.isEmpty)
|
||||
const Text(
|
||||
"Nessun membro dello staff configurato per questo store.\nVai in Anagrafica > Negozi per assegnare il personale.",
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
...storeStaff.map(
|
||||
(member) => ListTile(
|
||||
leading: const CircleAvatar(child: Icon(Icons.person)),
|
||||
title: Text(member.name),
|
||||
onTap: () {
|
||||
// 1. Inizializza il form nel Cubit
|
||||
context.read<OperationsCubit>().initOperationForm(
|
||||
existingOperation: OperationModel(
|
||||
storeId: currentStoreId,
|
||||
employeeId: member.id,
|
||||
number: '',
|
||||
createdAt: DateTime.now(),
|
||||
companyId: session.company!.id!,
|
||||
),
|
||||
);
|
||||
|
||||
// 2. Chiudi la modal
|
||||
Navigator.pop(modalContext);
|
||||
|
||||
// 3. Naviga verso il form
|
||||
context.pushNamed('operation-form');
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"welcomeBack": "Welcome back, {name}! 👋",
|
||||
"latestOperations": "Latest Operations",
|
||||
"masterData": "Master Data",
|
||||
"settings": "Settings",
|
||||
"newOperation": "Operation",
|
||||
"expiring_contracts": "Expiring Contracts",
|
||||
"sticky_notes": "Sticky Notes",
|
||||
"my_tasks": "My Tasks",
|
||||
"latest_operation_tickets": "Latest operation tickets"
|
||||
|
||||
}
|
||||
@@ -86,5 +86,6 @@
|
||||
"createCompanyScreenWillBeUsedForReceipts": "Verrà utilizzato per le tue stampe e ricevute",
|
||||
"createCompanyScreenSaveCompany": "SALVA AZIENDA",
|
||||
"createCompanyScreenSetupYourCompany": "Configura la tua Azienda",
|
||||
"createCompanyScreenFluxNeedsYourFiscalData": "FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi."
|
||||
"createCompanyScreenFluxNeedsYourFiscalData": "FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.",
|
||||
"operationFormAttachmentSectionNoCustomer": "Devi prima selezionare un cliente"
|
||||
}
|
||||
@@ -441,6 +441,12 @@ abstract class AppLocalizations {
|
||||
/// In it, this message translates to:
|
||||
/// **'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.'**
|
||||
String get createCompanyScreenFluxNeedsYourFiscalData;
|
||||
|
||||
/// No description provided for @operationFormAttachmentSectionNoCustomer.
|
||||
///
|
||||
/// In it, this message translates to:
|
||||
/// **'Devi prima selezionare un cliente'**
|
||||
String get operationFormAttachmentSectionNoCustomer;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -199,4 +199,8 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get createCompanyScreenFluxNeedsYourFiscalData =>
|
||||
'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.';
|
||||
|
||||
@override
|
||||
String get operationFormAttachmentSectionNoCustomer =>
|
||||
'Devi prima selezionare un cliente';
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1035,7 +1035,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||
|
||||
@@ -28,6 +28,7 @@ dependencies:
|
||||
qr_flutter: ^4.1.0
|
||||
shared_preferences: ^2.5.5
|
||||
supabase_flutter: ^2.12.2
|
||||
uuid: ^4.5.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user