Files
flux/lib/features/operations/data/operations_repository.dart

306 lines
9.3 KiB
Dart
Raw Normal View History

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
2026-04-29 19:25:48 +02:00
import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/attachments/models/attachment_model.dart';
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
2026-05-01 10:11:44 +02:00
import '../models/operation_model.dart';
2026-05-01 10:11:44 +02:00
class OperationsRepository {
final _supabase = Supabase.instance.client;
final companyId = GetIt.I.get<SessionCubit>().state.company!.id;
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
2026-05-01 10:11:44 +02:00
Future<OperationModel> fetchOperationById(String id) async {
try {
final response = await _supabase
2026-05-01 09:51:42 +02:00
.from('operation')
.select('''
*,
customer(name),
store(name),
staff_member(name),
provider(name),
model(name_with_brand),
attachment(*)
''')
.eq('id', id)
.single();
2026-05-01 10:11:44 +02:00
return OperationModel.fromMap(response);
} catch (e) {
throw Exception('Errore nel caricamento del servizio: $e');
}
}
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
2026-05-01 10:11:44 +02:00
Future<List<OperationModel>> fetchOperations({
required String companyId,
required int offset,
int limit = 50,
String? searchTerm,
DateTimeRange? dateRange,
}) async {
try {
var query = _supabase
2026-05-01 09:51:42 +02:00
.from('operation')
.select('''
*,
customer(name),
store(name),
provider(name),
model(name_with_brand),
staff_member(name),
attachment(*)
''')
.eq('company_id', companyId);
// Filtro Range Date
if (dateRange != null) {
query = query
.gte('created_at', dateRange.start.toIso8601String())
.lte('created_at', dateRange.end.toIso8601String());
}
if (searchTerm != null && searchTerm.isNotEmpty) {
// Filtra sui campi della tabella principale O su quelli della tabella joinata
query = query.or(
'reference.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.name.ilike.%$searchTerm%',
);
}
final response = await query
.order('created_at', ascending: false)
.range(offset, offset + limit - 1);
return (response as List)
2026-05-01 10:11:44 +02:00
.map((map) => OperationModel.fromMap(map))
.toList();
} catch (e) {
throw Exception('$e');
}
}
2026-05-01 10:11:44 +02:00
Stream<List<OperationModel>> getLastStoreOperationsStream({
2026-04-29 12:34:05 +02:00
required String storeId,
required int limit,
}) {
return _supabase
2026-05-01 09:51:42 +02:00
.from('operation')
2026-04-29 12:34:05 +02:00
.stream(primaryKey: ['id'])
.eq('store_id', storeId)
.order('created_at', ascending: false)
.limit(limit)
.map(
(listOfMaps) =>
2026-05-01 10:11:44 +02:00
listOfMaps.map((map) => OperationModel.fromMap(map)).toList(),
2026-04-29 12:34:05 +02:00
);
}
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
Future<OperationModel> saveFullOperation({
required OperationModel operation,
}) async {
try {
// 1. Salvataggio classico dell'operazione corrente
final response = await _supabase
2026-05-01 09:51:42 +02:00
.from('operation')
.upsert(operation.toMap())
.select(
'*, provider(*), model(*), store(*), staff_member(*), customer(*), attachment(*)',
)
.single();
final savedOperation = OperationModel.fromMap(response);
// 2. ALLINEAMENTO BATCH SEMPRE ATTIVO!
if (operation.batchUuid.isNotEmpty) {
await _supabase
.from('operation')
.update({'note': operation.note}) // Spalmiamo la nota attuale
.eq(
'batch_uuid',
operation.batchUuid,
); // Su tutte le pratiche di questo scontrino
}
return savedOperation;
} catch (e) {
throw Exception("Errore nel salvataggio dell'operazione: $e");
}
}
// --- ELIMINAZIONE ---
2026-05-01 10:11:44 +02:00
Future<void> deleteOperation(String id) async {
try {
2026-05-01 09:51:42 +02:00
await _supabase.from('operation').delete().eq('id', id);
} catch (e) {
throw Exception('$e');
}
}
// --- RECUPERO TIPI CONTENUTI PIÙ FREQUENTI PER AUTOCOMPLETE ---
Future<List<String>> fetchTopEntertainmentTypes(String companyId) async {
try {
// Cerchiamo i tipi più frequenti associati ai servizi di questa company
2026-05-01 09:51:42 +02:00
// Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id
final response = await _supabase
.from('operation')
.select('description')
.eq('company_id', companyId)
.eq('type', 'Entertainment')
.limit(50); // Prendiamo un campione
// Logica rapida per contare le occorrenze e prendere i primi 5
final Map<String, int> counts = {};
for (var item in (response as List)) {
final description = item['description'] as String;
counts[description] = (counts[description] ?? 0) + 1;
}
var sortedKeys = counts.keys.toList()
..sort((a, b) => counts[b]!.compareTo(counts[a]!));
return sortedKeys.take(5).toList();
} catch (e) {
return [
"Netflix",
"DAZN",
"Disney+",
"Sky",
]; // Fallback se non c'è ancora storia
}
}
/// Ascolta in tempo reale i file caricati per una pratica
Stream<List<AttachmentModel>> getOperationFilesStream(String operationId) {
return _supabase
.from('attachment')
.stream(primaryKey: ['id'])
2026-05-01 10:11:44 +02:00
.eq('operation_id', operationId)
.order('created_at', ascending: false)
.map(
(listOfMaps) =>
listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(),
);
}
Future<AttachmentModel> uploadAndRegisterOperationFile({
2026-05-01 10:11:44 +02:00
required String operationId,
required PlatformFile pickedFile,
}) async {
final cleanFileName = pickedFile.name.replaceAll(
RegExp(r'[^a-zA-Z0-9\.\-]'),
'_',
);
final storagePath =
2026-05-01 10:11:44 +02:00
'$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
final int fileSize = pickedFile.size;
final fileToSave = AttachmentModel(
companyId: GetIt.I.get<SessionCubit>().state.company!.id!,
2026-05-01 10:11:44 +02:00
operationId: operationId,
name: cleanFileName.fileNameWithoutExtension(),
extension: cleanFileName.fileExtension(),
storagePath: storagePath,
fileSize: fileSize,
);
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
? 'application/pdf'
: 'image/${fileToSave.extension}';
try {
// Usiamo bytes invece del path per massima compatibilità
if (pickedFile.bytes == null && pickedFile.path == null) {
throw 'Impossibile leggere il contenuto del file';
}
// Se siamo su desktop/mobile abbiamo il path, su web abbiamo i bytes
if (pickedFile.bytes != null) {
await _supabase.storage
.from('documents')
.uploadBinary(
storagePath,
pickedFile.bytes!,
fileOptions: FileOptions(contentType: mimeType, upsert: true),
);
}
final response = await _supabase
.from('attachment')
.insert(fileToSave.toMap())
.select()
.single();
return AttachmentModel.fromMap(response);
} catch (e) {
throw 'Errore durante l\'upload: $e';
}
}
Future<void> copyFileToCustomer({
required AttachmentModel file,
required String customerId,
}) async {
await _supabase
.from('attachment')
.update({'customer_id': customerId})
.eq('id', file.id!);
}
Future<void> renameAttachment(String id, String newName) async {
try {
await _supabase.from('attachment').update({'name': newName}).eq('id', id);
} catch (e) {
throw '$e';
}
}
Future<void> deleteSpecificOperationFile(AttachmentModel file) async {
try {
if (file.customerId == null) {
await _supabase.from('attachment').delete().eq('id', file.id!);
await _supabase.storage.from('documents').remove([file.storagePath!]);
} else {
await _supabase
.from('attachment')
.update({'operation_id': null})
.eq('id', file.id!);
}
} catch (e) {
throw '$e';
}
}
Future<void> deleteOperationFiles(List<AttachmentModel> files) async {
if (files.isEmpty) return;
// 1. Prepariamo le liste di ID e di Percorsi
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', idsToEdit);
}
} on PostgrestException catch (e) {
throw 'Errore database: ${e.message}';
} catch (e) {
throw 'Errore durante l\'eliminazione dei file: $e';
}
}
}