aaaaaaaaaaaa

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-02 10:22:47 +02:00
parent ac97e47771
commit 1721b2ff89
32 changed files with 454 additions and 1031 deletions

View File

@@ -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';

View File

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

View File

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

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

View File

@@ -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,
];
}

View File

@@ -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);
// 3. Cancellazione MASSIVA dallo Storage
await _supabase.storage.from('documents').remove(storagePaths);
if (idsToDelete.isNotEmpty) {
await _supabase.from('attachment').delete().inFilter('id', idsToDelete);
// 3. Cancellazione MASSIVA dallo Storage
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) {

View File

@@ -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,
];
}

View File

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

View File

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

View File

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