Files
flux/lib/features/attachments/data/attachments_repository.dart

200 lines
6.3 KiB
Dart
Raw Normal View History

import 'dart:typed_data';
import 'package:file_picker/file_picker.dart';
import 'package:flux/features/attachments/models/attachment_model.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
class AttachmentsRepository {
final _supabase = Supabase.instance.client;
static const String _bucketName = 'documents';
static const String _tableName =
'attachment'; // Cambia col vero nome della tua tabella se diverso!
/// Scarica i byte di un file direttamente da Supabase Storage
Future<Uint8List> downloadAttachmentBytes(String storagePath) async {
try {
final Uint8List bytes = await _supabase.storage
.from(_bucketName)
.download(storagePath);
return bytes;
} catch (e) {
throw Exception("Impossibile scaricare il documento dal cloud: $e");
}
}
/// RESTITUISCE IL NOME DELLA COLONNA DB IN BASE AL TIPO
String _getColumnNameForParent(AttachmentParentType parentType) {
switch (parentType) {
case AttachmentParentType.operation:
return 'operation_id';
case AttachmentParentType.ticket:
return 'ticket_id';
case AttachmentParentType.customer:
return 'customer_id';
}
}
/// RECUPERA I FILE IN TEMPO REALE
Stream<List<AttachmentModel>> getFilesStream(
String parentId,
AttachmentParentType parentType,
) {
final columnName = _getColumnNameForParent(parentType);
return _supabase
.from(_tableName)
.stream(primaryKey: ['id'])
.eq(columnName, parentId)
.map(
(listOfMaps) =>
listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(),
);
}
/// CARICA IL FILE NELLO STORAGE E LO REGISTRA NEL DB
Future<void> uploadAndRegisterFile({
required String parentId,
required AttachmentParentType parentType,
required PlatformFile pickedFile,
2026-05-09 09:50:20 +02:00
required String companyId,
}) async {
try {
2026-05-09 10:20:53 +02:00
if (pickedFile.bytes == null) {
throw Exception(
"I bytes del file sono vuoti! Ricarica la pagina senza cache.",
);
}
final extension = pickedFile.extension ?? pickedFile.name.split('.').last;
final cleanName = pickedFile.name
.replaceAll(RegExp(r'[^\w\s\.-]'), '')
.replaceAll(' ', '_');
// Creiamo un path ordinato: idAzienda/tipoEntita/idEntita/timestamp_nomefile
final timestamp = DateTime.now().millisecondsSinceEpoch;
final storagePath =
'$companyId/${parentType.name}/$parentId/${timestamp}_$cleanName';
// 1. Upload su Supabase Storage
await _supabase.storage
.from(_bucketName)
.uploadBinary(
storagePath,
pickedFile.bytes!,
fileOptions: FileOptions(contentType: _guessContentType(extension)),
);
// 2. Creiamo la mappa per il DB dinamicamente
final Map<String, dynamic> insertData = {
'company_id': companyId,
'name': pickedFile.name.replaceAll('.$extension', ''),
'extension': extension,
'file_size': pickedFile.size,
'storage_path': storagePath,
};
// Inseriamo l'ID nella colonna giusta!
final columnName = _getColumnNameForParent(parentType);
insertData[columnName] = parentId;
// 3. Salviamo su Postgres
await _supabase.from(_tableName).insert(insertData);
} catch (e) {
2026-05-09 10:20:53 +02:00
throw Exception("Errore caricamento: $e");
}
}
/// ELIMINA IL FILE (Scollegamento intelligente)
Future<void> deleteFiles({
required List<AttachmentModel> files,
required AttachmentParentType currentContextType,
}) async {
if (files.isEmpty) return;
try {
for (var file in files) {
if (file.id == null) continue;
// 1. Capiamo quali collegamenti ha questo file attualmente
final currentLinks = {
AttachmentParentType.operation: file.operationId,
AttachmentParentType.ticket: file.ticketId,
AttachmentParentType.customer: file.customerId,
};
// 2. Simuliamo la rimozione del collegamento per il contesto attuale
currentLinks[currentContextType] = null;
// 3. Controlliamo se rimangono altri ID valorizzati
final hasOtherActiveLinks = currentLinks.values.any(
(id) => id != null && id.isNotEmpty,
);
if (hasOtherActiveLinks) {
// A. Ci sono ancora altre entità che usano questo file!
// Scolleghiamolo SOLO dal contesto attuale mettendo a NULL la sua colonna
await _supabase
.from(_tableName)
.update({currentContextType.dbColumn: null})
.eq('id', file.id!);
} else {
// B. Nessuno usa più questo file! ELIMINAZIONE FISICA TOTALE.
await _supabase.from(_tableName).delete().eq('id', file.id!);
if (file.storagePath != null) {
await _supabase.storage.from(_bucketName).remove([
file.storagePath!,
]);
}
}
}
} catch (e) {
throw Exception("Errore nell'eliminazione dei file: $e");
}
}
/// RINOMINA UN FILE (Solo nel DB, non cambiamo il file fisico)
Future<void> renameAttachment(String fileId, String newName) async {
try {
await _supabase
.from(_tableName)
.update({'name': newName})
.eq('id', fileId);
} catch (e) {
throw Exception("Errore nella rinomina del file: $e");
}
}
/// ASSOCIA UN FILE A UN'ALTRA ENTITÀ (Modifica il record esistente)
Future<void> linkFileToEntity({
required String fileId,
required AttachmentParentType targetType,
required String targetId,
}) async {
try {
// Facciamo un semplice UPDATE aggiungendo l'ID nella colonna giusta
await _supabase
.from(_tableName)
.update({targetType.dbColumn: targetId})
.eq('id', fileId);
} catch (e) {
throw Exception("Errore nel collegamento del file: $e");
}
}
// Helper per indovinare il content-type base
String _guessContentType(String extension) {
switch (extension.toLowerCase()) {
case 'pdf':
return 'application/pdf';
case 'png':
return 'image/png';
case 'jpg':
case 'jpeg':
return 'image/jpeg';
default:
return 'application/octet-stream';
}
}
}