Files
flux/lib/features/operations/data/operations_repository.dart
Mark M2 Macbook 4efc3ce182
All checks were successful
Build and Release FLUX (Multi-Platform) / build-android (push) Successful in 2m11s
Build and Release FLUX (Multi-Platform) / build-web (push) Successful in 1m1s
Build and Release FLUX (Multi-Platform) / build-windows (push) Successful in 8m5s
mmmh
2026-06-04 12:34:38 +02:00

407 lines
12 KiB
Dart

import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/enums_and_consts/consts.dart';
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';
import '../models/operation_model.dart';
class OperationsRepository {
final _supabase = Supabase.instance.client;
final companyId = GetIt.I.get<SessionCubit>().state.company!.id;
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
Future<OperationModel> fetchOperationById(String id) async {
try {
final response = await _supabase
.from(Tables.operations)
.select('''
*,
${Tables.customers}(*),
${Tables.stores}(name),
${Tables.staffMembers}(name),
${Tables.providers}(name),
${Tables.models}(name_with_brand),
${Tables.attachments}(*)
''')
.eq('id', id)
.single();
return OperationModel.fromMap(response);
} catch (e) {
throw Exception('Errore nel caricamento del servizio: $e');
}
}
// 🥷 2. RECUPERO PAGINATO ASSOLUTO CON CONTEGGIO TOTALI
Future<PaginatedOperations> fetchPaginatedOperations({
required String companyId,
String? storeId,
String? staffId,
String? providerId,
required int page, // Usiamo 'page' (1, 2, 3...) invece di 'offset'
int itemsPerPage = 25, // Default a 25 elementi per pagina
String? searchTerm,
DateTimeRange? dateRange,
}) async {
try {
// Calcoliamo il range di partenza e fine per Supabase
// Es. Pagina 1, 25 items -> range(0, 24)
// Es. Pagina 2, 25 items -> range(25, 49)
final from = (page - 1) * itemsPerPage;
final to = from + itemsPerPage - 1;
var query = _supabase
.from(Tables.operations)
.select('''
*,
${Tables.customers}(*),
${Tables.stores}(name),
${Tables.providers}(name, color_hex),
${Tables.models}(name_with_brand),
${Tables.staffMembers}(name),
${Tables.attachments}(*)
''')
.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 (storeId != null) {
query = query.or('store_id.eq.$storeId,store_id.is.null');
}
if (staffId != null) {
query = query.or('staff_id.eq.$staffId,staff_id.is.null');
}
if (providerId != null) {
query = query.or('provider_id.eq.$providerId,provider_id.is.null');
}
if (searchTerm != null && searchTerm.isNotEmpty) {
query = query.or(
'reference.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.name.ilike.%$searchTerm%',
);
}
final response = await query
.order('created_at', ascending: false)
.range(from, to)
.count(CountOption.exact);
// 3. Estrazione dei dati
final List<OperationModel> operations = (response.data as List)
.map((map) => OperationModel.fromMap(map))
.toList();
final int totalCount = response.count;
return PaginatedOperations(
operations: operations,
totalCount: totalCount,
);
} catch (e) {
throw Exception('Errore nel recupero della pagina $page: $e');
}
}
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
/* Future<List<OperationModel>> fetchOperations({
required String companyId,
String? storeId,
String? staffId,
String? providerId,
required int offset,
int limit = 50,
String? searchTerm,
DateTimeRange? dateRange,
}) async {
try {
var query = _supabase
.from(Tables.operations)
.select('''
*,
${Tables.customers}(*),
${Tables.stores}(name),
${Tables.providers}(name),
${Tables.models}(name_with_brand),
${Tables.staffMembers}(name),
${Tables.attachments}(*)
''')
.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 (storeId != null) {
query = query.or('store_id.eq.$storeId,store_id.is.null');
}
if (staffId != null) {
query = query.or('staff_id.eq.$staffId,staff_id.is.null');
}
if (providerId != null) {
query = query.or('provider_id.eq.$providerId,provider_id.is.null');
}
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)
.map((map) => OperationModel.fromMap(map))
.toList();
} catch (e) {
throw Exception('$e');
}
} */
Stream<List<Map<String, dynamic>>> watchStoreOperations({
required String storeId,
required int limit,
}) {
return _supabase
.from(Tables.operations)
.stream(primaryKey: ['id'])
.eq('store_id', storeId)
.order('created_at', ascending: false)
.limit(limit);
}
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
Future<OperationModel> saveFullOperation({
required OperationModel operation,
}) async {
try {
// 1. Salvataggio classico dell'operazione corrente
final response = await _supabase
.from(Tables.operations)
.upsert(operation.toMap())
.select(
'*, ${Tables.providers}(*), ${Tables.models}(*), ${Tables.stores}(*), ${Tables.staffMembers}(*), ${Tables.customers}(*), ${Tables.attachments}(*)',
)
.single();
final savedOperation = OperationModel.fromMap(response);
// 2. ALLINEAMENTO BATCH SEMPRE ATTIVO!
if (operation.batchUuid.isNotEmpty) {
await _supabase
.from(Tables.operations)
.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 ---
Future<void> deleteOperation(String id) async {
try {
await _supabase.from(Tables.operations).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
// Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id
final response = await _supabase
.from(Tables.operations)
.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(Tables.attachments)
.stream(primaryKey: ['id'])
.eq('operation_id', operationId)
.order('created_at', ascending: false)
.map(
(listOfMaps) =>
listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(),
);
}
Future<AttachmentModel> uploadAndRegisterOperationFile({
required String operationId,
required PlatformFile pickedFile,
}) async {
final cleanFileName = pickedFile.name.replaceAll(
RegExp(r'[^a-zA-Z0-9\.\-]'),
'_',
);
final storagePath =
'$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
final int fileSize = pickedFile.size;
final fileToSave = AttachmentModel(
companyId: GetIt.I.get<SessionCubit>().state.company!.id!,
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(Tables.attachments)
.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(Tables.attachments)
.update({'customer_id': customerId})
.eq('id', file.id!);
}
Future<void> renameAttachment(String id, String newName) async {
try {
await _supabase
.from(Tables.attachments)
.update({'name': newName})
.eq('id', id);
} catch (e) {
throw '$e';
}
}
Future<void> deleteSpecificOperationFile(AttachmentModel file) async {
try {
if (file.customerId == null) {
await _supabase.from(Tables.attachments).delete().eq('id', file.id!);
await _supabase.storage.from('documents').remove([file.storagePath!]);
} else {
await _supabase
.from(Tables.attachments)
.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(Tables.attachments)
.delete()
.inFilter('id', idsToDelete);
await _supabase.storage.from('documents').remove(storagePathsToDelete);
}
if (idsToEdit.isNotEmpty) {
await _supabase
.from(Tables.attachments)
.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';
}
}
}
class PaginatedOperations {
final List<OperationModel> operations;
final int totalCount;
PaginatedOperations({required this.operations, required this.totalCount});
}