243 lines
7.8 KiB
Dart
243 lines
7.8 KiB
Dart
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';
|
|
|
|
enum Bucket {
|
|
documents('documents'),
|
|
companyDocuments('company_documents');
|
|
|
|
final String value;
|
|
const Bucket(this.value);
|
|
}
|
|
|
|
class AttachmentsRepository {
|
|
final _supabase = Supabase.instance.client;
|
|
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({
|
|
required String storagePath,
|
|
required Bucket bucket,
|
|
}) async {
|
|
try {
|
|
final Uint8List bytes = await _supabase.storage
|
|
.from(bucket.value)
|
|
.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';
|
|
case AttachmentParentType.shippingDocument:
|
|
return 'shipping_document_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 String companyId,
|
|
required Bucket bucket,
|
|
PlatformFile? pickedFile, // Ora è opzionale
|
|
Uint8List? rawBytes, // Alternativa: bytes grezzi
|
|
String? rawFileName, // Alternativa: nome del file
|
|
}) async {
|
|
// 🛡️ L'ASSERT NINJA: O c'è il pickedFile, o ci sono i byte e il nome.
|
|
// L'assert funziona solo in debug, ma è perfetto per beccare subito errori di chiamata!
|
|
assert(
|
|
pickedFile != null || (rawBytes != null && rawFileName != null),
|
|
'Devi passare o un PlatformFile, oppure rawBytes e rawFileName!',
|
|
);
|
|
|
|
try {
|
|
// 1. Normalizziamo i dati in base a cosa ci è stato passato
|
|
final Uint8List finalBytes;
|
|
final String finalFileName;
|
|
final int finalFileSize;
|
|
|
|
if (pickedFile != null) {
|
|
if (pickedFile.bytes == null) {
|
|
throw Exception(
|
|
"I bytes del file sono vuoti! Ricarica la pagina senza cache.",
|
|
);
|
|
}
|
|
finalBytes = pickedFile.bytes!;
|
|
finalFileName = pickedFile.name;
|
|
finalFileSize = pickedFile.size;
|
|
} else {
|
|
// Se pickedFile è null, grazie all'assert sappiamo che questi non lo sono
|
|
finalBytes = rawBytes!;
|
|
finalFileName = rawFileName!;
|
|
finalFileSize = finalBytes.length; // Calcoliamo la size dai byte reali
|
|
}
|
|
|
|
// 2. Estraiamo l'estensione e puliamo il nome
|
|
final extension = finalFileName.contains('.')
|
|
? finalFileName.split('.').last
|
|
: ''; // Fallback se il file non ha estensione
|
|
|
|
final cleanName = finalFileName
|
|
.replaceAll(RegExp(r'[^\w\s\.-]'), '')
|
|
.replaceAll(' ', '_');
|
|
|
|
// 3. Creiamo un path ordinato: idAzienda/tipoEntita/idEntita/timestamp_nomefile
|
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
|
final storagePath =
|
|
'$companyId/${parentType.name}/$parentId/${timestamp}_$cleanName';
|
|
|
|
// 4. Upload su Supabase Storage
|
|
await _supabase.storage
|
|
.from(bucket.value)
|
|
.uploadBinary(
|
|
storagePath,
|
|
finalBytes,
|
|
fileOptions: FileOptions(contentType: _guessContentType(extension)),
|
|
);
|
|
|
|
// 5. Creiamo la mappa per il DB dinamicamente
|
|
final Map<String, dynamic> insertData = {
|
|
'company_id': companyId,
|
|
'name': finalFileName.replaceAll('.$extension', ''),
|
|
'extension': extension,
|
|
'file_size': finalFileSize,
|
|
'storage_path': storagePath,
|
|
};
|
|
|
|
// Inseriamo l'ID nella colonna giusta!
|
|
final columnName = _getColumnNameForParent(parentType);
|
|
insertData[columnName] = parentId;
|
|
|
|
// 6. Salviamo su Postgres
|
|
await _supabase.from(_tableName).insert(insertData);
|
|
} catch (e) {
|
|
throw Exception("Errore caricamento: $e");
|
|
}
|
|
}
|
|
|
|
/// ELIMINA IL FILE (Scollegamento intelligente)
|
|
Future<void> deleteFiles({
|
|
required List<AttachmentModel> files,
|
|
required AttachmentParentType currentContextType,
|
|
required Bucket bucket,
|
|
}) 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,
|
|
AttachmentParentType.shippingDocument: file.shippingDocumentId,
|
|
};
|
|
|
|
// 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(bucket.value).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';
|
|
}
|
|
}
|
|
}
|