feat-insert-service (#5)
Reviewed-on: http://catelliub.zapto.org:3000/brontomark/flux/pulls/5 Co-authored-by: mark-cachy <marco@catelli.it> Co-committed-by: mark-cachy <marco@catelli.it>
This commit is contained in:
@@ -1,9 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||
import 'package:flux/features/services/models/service_file_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../models/service_model.dart';
|
||||
|
||||
class ServicesRepository {
|
||||
final _supabase = Supabase.instance.client;
|
||||
final companyId = GetIt.I.get<SessionBloc>().state.company!.id;
|
||||
final CustomerRepository _customerRepository = GetIt.I<CustomerRepository>();
|
||||
|
||||
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
|
||||
Future<ServiceModel> fetchServiceById(String id) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('service')
|
||||
.select('''
|
||||
*,
|
||||
customer(nome),
|
||||
energy_service(*),
|
||||
fin_service(*),
|
||||
entertainment_service(*),
|
||||
service_file(*)
|
||||
''')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
return ServiceModel.fromMap(response);
|
||||
} catch (e) {
|
||||
throw Exception('Errore nel caricamento del servizio: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
|
||||
Future<List<ServiceModel>> fetchServices({
|
||||
@@ -19,10 +48,11 @@ class ServicesRepository {
|
||||
.from('service')
|
||||
.select('''
|
||||
*,
|
||||
customer(name, surname),
|
||||
customer(nome),
|
||||
energy_service(*),
|
||||
fin_service(*),
|
||||
entertainment_service(*)
|
||||
entertainment_service(*),
|
||||
service_file(*)
|
||||
''')
|
||||
.eq('company_id', companyId);
|
||||
|
||||
@@ -36,7 +66,7 @@ class ServicesRepository {
|
||||
if (searchTerm != null && searchTerm.isNotEmpty) {
|
||||
// Filtra sui campi della tabella principale O su quelli della tabella joinata
|
||||
query = query.or(
|
||||
'number.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.name.ilike.%$searchTerm%,customer.surname.ilike.%$searchTerm%',
|
||||
'number.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.nome.ilike.%$searchTerm%',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,8 +85,7 @@ class ServicesRepository {
|
||||
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
||||
Future<void> saveFullService(ServiceModel service) async {
|
||||
try {
|
||||
// 1. Inseriamo il record principale
|
||||
// Se service.id è null, Supabase fa INSERT. Se c'è, fa UPDATE (grazie all'upsert o gestione manuale)
|
||||
// 1. Upsert del record principale
|
||||
final serviceData = await _supabase
|
||||
.from('service')
|
||||
.upsert(service.toMap())
|
||||
@@ -65,45 +94,103 @@ class ServicesRepository {
|
||||
|
||||
final String newId = serviceData['id'];
|
||||
|
||||
// 2. Pulizia vecchi record figli (necessaria se è una MODIFICA)
|
||||
// Se stiamo modificando, cancelliamo i vecchi per reinserire i nuovi (più semplice)
|
||||
// 2. MODIFICA: Pulizia atomica dei figli
|
||||
// Se stiamo modificando (id != null), resettiamo le tabelle collegate
|
||||
if (service.id != null) {
|
||||
await _supabase.from('energy_service').delete().eq('service_id', newId);
|
||||
await _supabase.from('fin_service').delete().eq('service_id', newId);
|
||||
await _supabase
|
||||
.from('entertainment_service')
|
||||
.delete()
|
||||
.eq('service_id', newId);
|
||||
await Future.wait([
|
||||
_supabase.from('energy_service').delete().eq('service_id', newId),
|
||||
_supabase.from('fin_service').delete().eq('service_id', newId),
|
||||
_supabase
|
||||
.from('entertainment_service')
|
||||
.delete()
|
||||
.eq('service_id', newId),
|
||||
// Aggiungi qui eventuali altre tabelle pivot o file
|
||||
]);
|
||||
}
|
||||
|
||||
// 3. Inserimento EnergyServices
|
||||
// 3. Inserimento dei moduli in parallelo per velocità
|
||||
final List<Future> insertTasks = [];
|
||||
|
||||
if (service.energyServices.isNotEmpty) {
|
||||
final List<Map<String, dynamic>> toInsert = [];
|
||||
for (var item in service.energyServices) {
|
||||
toInsert.add(item.copyWith(serviceId: newId).toMap());
|
||||
}
|
||||
await _supabase.from('energy_service').insert(toInsert);
|
||||
insertTasks.add(
|
||||
_supabase
|
||||
.from('energy_service')
|
||||
.insert(
|
||||
service.energyServices
|
||||
.map((item) => item.copyWith(serviceId: newId).toMap())
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Inserimento FinServices
|
||||
if (service.finServices.isNotEmpty) {
|
||||
final List<Map<String, dynamic>> toInsert = [];
|
||||
for (var item in service.finServices) {
|
||||
toInsert.add(item.copyWith(serviceId: newId).toMap());
|
||||
}
|
||||
await _supabase.from('fin_service').insert(toInsert);
|
||||
insertTasks.add(
|
||||
_supabase
|
||||
.from('fin_service')
|
||||
.insert(
|
||||
service.finServices
|
||||
.map((item) => item.copyWith(serviceId: newId).toMap())
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Inserimento EntertainmentServices
|
||||
if (service.entertainmentServices.isNotEmpty) {
|
||||
final List<Map<String, dynamic>> toInsert = [];
|
||||
for (var item in service.entertainmentServices) {
|
||||
toInsert.add(item.copyWith(serviceId: newId).toMap());
|
||||
insertTasks.add(
|
||||
_supabase
|
||||
.from('entertainment_service')
|
||||
.insert(
|
||||
service.entertainmentServices
|
||||
.map((item) => item.copyWith(serviceId: newId).toMap())
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (insertTasks.isNotEmpty) {
|
||||
await Future.wait(insertTasks);
|
||||
}
|
||||
if (service.files.isNotEmpty) {
|
||||
final List<Future> uploadTasks = [];
|
||||
|
||||
for (var file in service.files) {
|
||||
final storagePath =
|
||||
'$companyId/services/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}';
|
||||
final String mimeType = file.extension.toLowerCase() == 'pdf'
|
||||
? 'application/pdf'
|
||||
: 'image/${file.extension}';
|
||||
final fileToSave = file.copyWith(serviceId: newId, url: storagePath);
|
||||
|
||||
// Creiamo una funzione asincrona per caricare file e scrivere nel DB
|
||||
Future<void> uploadAndLink() async {
|
||||
// Determiniamo il MIME type corretto in base all'estensione
|
||||
|
||||
// A. Upload nel Bucket Storage (usiamo i bytes così funziona anche su Web!)
|
||||
await _supabase.storage
|
||||
.from('documents')
|
||||
.uploadBinary(
|
||||
storagePath,
|
||||
fileToSave.localBytes!,
|
||||
fileOptions: FileOptions(
|
||||
contentType:
|
||||
mimeType, // Diciamo a Supabase esattamente cos'è!
|
||||
upsert:
|
||||
true, // Opzionale: sovrascrive se esiste già un file con lo stesso nome
|
||||
),
|
||||
);
|
||||
|
||||
await _supabase.from('service_file').insert(fileToSave.toMap());
|
||||
}
|
||||
|
||||
uploadTasks.add(uploadAndLink());
|
||||
}
|
||||
await _supabase.from('entertainment_service').insert(toInsert);
|
||||
|
||||
// Eseguiamo tutti gli upload in parallelo per la massima velocità
|
||||
await Future.wait(uploadTasks);
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Errore durante il salvataggio: $e');
|
||||
// Qui potresti aggiungere una logica di "rollback manuale" se necessario
|
||||
throw Exception('Errore durante il salvataggio corazzato: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,4 +202,50 @@ class ServicesRepository {
|
||||
throw Exception('Errore durante l\'eliminazione: $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 'service' per filtrare per company_id
|
||||
final response = await _supabase
|
||||
.from('entertainment_service')
|
||||
.select('type, service!inner(store!inner(company_id))')
|
||||
.eq('service.store.company_id', companyId)
|
||||
.limit(100); // 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 type = item['type'] as String;
|
||||
counts[type] = (counts[type] ?? 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
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> copyFileToCustomer({
|
||||
required ServiceFileModel file,
|
||||
required String customerId,
|
||||
}) async {
|
||||
CustomerFileModel fileToCopy = CustomerFileModel(
|
||||
customerId: customerId,
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
extension: file.extension,
|
||||
fileSize: file.fileSize,
|
||||
);
|
||||
await _customerRepository.saveCustomerFile(fileToCopy);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user