Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-01 11:54:39 +02:00
parent f8bcac51e1
commit ac97e47771
9 changed files with 358 additions and 321 deletions

View File

@@ -2,6 +2,7 @@ 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/utils/extensions.dart';
import 'package:flux/features/attachments/models/attachment_model.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:flux/features/operations/models/operation_file_model.dart';
@@ -21,11 +22,8 @@ class OperationsRepository {
.from('operation')
.select('''
*,
customer(nome),
energy_operation(*),
fin_operation(*),
entertainment_operation(*),
operation_file(*)
customer(name),
staff_member(name)
''')
.eq('id', id)
.single();
@@ -45,16 +43,13 @@ class OperationsRepository {
DateTimeRange? dateRange,
}) async {
try {
// Nota: 'customer(name, surname)' serve per il display name nella card
var query = _supabase
.from('operation')
.select('''
*,
customer(nome),
energy_operation(*),
fin_operation(*),
entertainment_operation(*),
operation_file(*)
customer(name),
staff_member(name),
attachments(*)
''')
.eq('company_id', companyId);
@@ -68,7 +63,7 @@ class OperationsRepository {
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.nome.ilike.%$searchTerm%',
'reference.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.name.ilike.%$searchTerm%',
);
}
@@ -80,7 +75,7 @@ class OperationsRepository {
.map((map) => OperationModel.fromMap(map))
.toList();
} catch (e) {
throw Exception('Errore nel caricamento servizi: $e');
throw Exception('$e');
}
}
@@ -112,66 +107,9 @@ class OperationsRepository {
final String newId = operationData['id'];
// 2. MODIFICA: Pulizia atomica dei figli
// Se stiamo modificando (id != null), resettiamo le tabelle collegate
if (operation.id != null) {
await Future.wait([
_supabase.from('energy_operation').delete().eq('operation_id', newId),
_supabase.from('fin_operation').delete().eq('operation_id', newId),
_supabase
.from('entertainment_operation')
.delete()
.eq('operation_id', newId),
// Aggiungi qui eventuali altre tabelle pivot o file
]);
}
// 3. Inserimento dei moduli in parallelo per velocità
final List<Future> insertTasks = [];
if (operation.energyOperations.isNotEmpty) {
insertTasks.add(
_supabase
.from('energy_operation')
.insert(
operation.energyOperations
.map((item) => item.copyWith(operationId: newId).toMap())
.toList(),
),
);
}
if (operation.finOperations.isNotEmpty) {
insertTasks.add(
_supabase
.from('fin_operation')
.insert(
operation.finOperations
.map((item) => item.copyWith(operationId: newId).toMap())
.toList(),
),
);
}
if (operation.entertainmentOperations.isNotEmpty) {
insertTasks.add(
_supabase
.from('entertainment_operation')
.insert(
operation.entertainmentOperations
.map((item) => item.copyWith(operationId: newId).toMap())
.toList(),
),
);
}
if (insertTasks.isNotEmpty) {
await Future.wait(insertTasks);
}
// 4. UPLOAD DEI FILE LOCALI (Nuovi)
// Filtriamo solo i file che non hanno ancora un ID (quindi sono locali)
final localFilesToUpload = operation.files
final localFilesToUpload = operation.attachments
.where((f) => f.id == null)
.toList();
@@ -202,7 +140,7 @@ class OperationsRepository {
);
// B. Inserimento riga nel DB relazionale
await _supabase.from('operation_file').insert(fileToSave.toMap());
await _supabase.from('attachment').insert(fileToSave.toMap());
}
uploadTasks.add(uploadAndLink());
@@ -219,10 +157,9 @@ class OperationsRepository {
.from('operation')
.select('''
*,
energy_operation(*),
fin_operation(*),
entertainment_operation(*),
operation_file(*)
staff_member(name),
customer(name),
attachments(*)
''')
.eq('id', newId)
.single();
@@ -230,7 +167,7 @@ class OperationsRepository {
return OperationModel.fromMap(updatedOperationData);
} catch (e) {
// Qui potresti aggiungere una logica di "rollback manuale" se necessario
throw Exception('Errore durante il salvataggio corazzato: $e');
throw Exception('$e');
}
}
@@ -239,7 +176,7 @@ class OperationsRepository {
try {
await _supabase.from('operation').delete().eq('id', id);
} catch (e) {
throw Exception('Errore durante l\'eliminazione: $e');
throw Exception('$e');
}
}
@@ -249,16 +186,17 @@ class OperationsRepository {
// 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('entertainment_operation')
.select('type, operation!inner(store!inner(company_id))')
.eq('operation.store.company_id', companyId)
.limit(100); // Prendiamo un campione
.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 type = item['type'] as String;
counts[type] = (counts[type] ?? 0) + 1;
final description = item['description'] as String;
counts[description] = (counts[description] ?? 0) + 1;
}
var sortedKeys = counts.keys.toList()
@@ -276,19 +214,19 @@ class OperationsRepository {
}
/// Ascolta in tempo reale i file caricati per una pratica
Stream<List<OperationFileModel>> getOperationFilesStream(String operationId) {
Stream<List<AttachmentModel>> getOperationFilesStream(String operationId) {
return _supabase
.from('operation_file')
.from('attachment')
.stream(primaryKey: ['id'])
.eq('operation_id', operationId)
.order('created_at', ascending: false)
.map(
(listOfMaps) =>
listOfMaps.map((map) => OperationFileModel.fromMap(map)).toList(),
listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(),
);
}
Future<OperationFileModel> uploadAndRegisterOperationFile({
Future<AttachmentModel> uploadAndRegisterOperationFile({
required String operationId,
required PlatformFile pickedFile,
}) async {
@@ -299,7 +237,8 @@ class OperationsRepository {
final storagePath =
'$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
final int fileSize = pickedFile.size;
final fileToSave = OperationFileModel(
final fileToSave = AttachmentModel(
companyId: GetIt.I.get<SessionCubit>().state.company!.id!,
operationId: operationId,
name: cleanFileName.fileNameWithoutExtension(),
extension: cleanFileName.fileExtension(),
@@ -327,12 +266,12 @@ class OperationsRepository {
}
final response = await _supabase
.from('operation_file')
.from('attachment')
.insert(fileToSave.toMap())
.select()
.single();
return OperationFileModel.fromMap(response);
return AttachmentModel.fromMap(response);
} catch (e) {
throw 'Errore durante l\'upload: $e';
}
@@ -342,17 +281,13 @@ class OperationsRepository {
required OperationFileModel file,
required String customerId,
}) async {
CustomerFileModel fileToCopy = CustomerFileModel(
customerId: customerId,
name: file.name,
storagePath: file.storagePath,
extension: file.extension,
fileSize: file.fileSize,
);
await _customerRepository.saveFileReference(fileToCopy);
await _supabase
.from('attachment')
.update({'customer_id': customerId})
.eq('id', file.id!);
}
Future<void> deleteOperationFiles(List<OperationFileModel> files) async {
Future<void> deleteOperationFiles(List<AttachmentModel> files) async {
if (files.isEmpty) return;
// 1. Prepariamo le liste di ID e di Percorsi
final List<String> idsToDelete = files.map((f) => f.id!).toList();
@@ -360,18 +295,14 @@ class OperationsRepository {
try {
await _supabase
.from('operation_file')
.delete()
.from('attachment')
.update({'operation_id': null})
.inFilter('id', idsToDelete);
await _supabase.storage.from('documents').remove(storagePaths);
debugPrint("Eliminati con successo ${files.length} file.");
} on PostgrestException catch (e) {
debugPrint("Errore DB: ${e.message}");
throw 'Errore database: ${e.message}';
} catch (e) {
debugPrint("Errore generico: $e");
throw 'Errore durante l\'eliminazione dei file: $e';
}
}

View File

@@ -1,200 +1,200 @@
import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/operations/models/energy_operation_model.dart';
import 'package:flux/features/operations/models/entertainment_operation_model.dart';
import 'package:flux/features/operations/models/fin_operation_model.dart';
import 'package:flux/features/operations/models/operation_file_model.dart'; // <-- Aggiunto Import
import 'package:flux/features/attachments/models/attachment_model.dart';
enum OperationStatus {
ok('ok'),
waitingforaction('waiting_for_action'),
waitingforsupport('waiting_for_support'),
waitingfordeployment('waiting_for_deployment'),
ko('ko'),
draft('draft'),
canceled('canceled');
static OperationStatus fromString(String value) {
final normalizedValue = value.replaceAll('_', '').toLowerCase();
return OperationStatus.values.firstWhere(
(e) => e.name.toLowerCase() == normalizedValue,
);
}
final String supabaseName;
const OperationStatus(this.supabaseName);
}
class OperationModel extends Equatable {
final String? id;
final DateTime? createdAt;
final String storeId;
final String? employeeId;
final String? customerId;
final String number;
final bool isBozza;
final String type;
final String? providerId;
final String? modelId;
final String? description;
final DateTime? expirationDate;
final String note;
final bool resultOk;
final String? customerDisplayName;
final bool showInDashboard;
final String batchUuid;
final String companyId;
// Telefonia
final int al;
final int mnp;
final int nip;
final int unica;
final int telepass;
// Moduli (Liste)
final List<EnergyOperationModel> energyOperations;
final List<FinOperationModel> finOperations;
final List<EntertainmentOperationModel> entertainmentOperations;
final String storeId;
final int quantity;
final String? staffId;
final String staffDisplayName;
final String? lastCampaignId;
final OperationStatus status;
final String? customerId;
final String customerDisplayName;
final String reference;
// ALLEGATI (Aggiunto)
final List<OperationFileModel> files;
final List<AttachmentModel> attachments;
const OperationModel({
this.id,
this.createdAt,
required this.storeId,
this.employeeId,
this.customerId,
required this.number,
this.isBozza = true,
this.type = '',
this.providerId,
this.modelId,
this.description,
this.expirationDate,
this.note = '',
this.resultOk = true,
this.al = 0,
this.mnp = 0,
this.nip = 0,
this.unica = 0,
this.telepass = 0,
this.energyOperations = const [],
this.finOperations = const [],
this.entertainmentOperations = const [],
this.files = const [], // <-- Aggiunto default vuoto
this.customerDisplayName,
this.showInDashboard = true,
this.batchUuid = '',
required this.companyId,
this.storeId = '',
this.quantity = 1,
this.staffId,
this.staffDisplayName = '',
this.lastCampaignId,
this.status = OperationStatus.draft,
this.customerId,
this.customerDisplayName = '',
this.reference = '',
this.attachments = const [],
});
OperationModel copyWith({
String? id,
DateTime? createdAt,
String? storeId,
String? employeeId,
String? customerId,
String? number,
bool? isBozza,
String? type,
String? providerId,
String? modelId,
String? description,
DateTime? expirationDate,
String? note,
bool? resultOk,
int? al,
int? mnp,
int? nip,
int? unica,
int? telepass,
List<EnergyOperationModel>? energyOperations,
List<FinOperationModel>? finOperations,
List<EntertainmentOperationModel>? entertainmentOperations,
List<OperationFileModel>? files, // <-- Aggiunto
String? customerDisplayName,
bool? showInDashboard,
String? batchUuid,
String? companyId,
}) {
return OperationModel(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
storeId: storeId ?? this.storeId,
employeeId: employeeId ?? this.employeeId,
customerId: customerId ?? this.customerId,
number: number ?? this.number,
isBozza: isBozza ?? this.isBozza,
note: note ?? this.note,
resultOk: resultOk ?? this.resultOk,
al: al ?? this.al,
mnp: mnp ?? this.mnp,
nip: nip ?? this.nip,
unica: unica ?? this.unica,
telepass: telepass ?? this.telepass,
energyOperations: energyOperations ?? this.energyOperations,
finOperations: finOperations ?? this.finOperations,
entertainmentOperations:
entertainmentOperations ?? this.entertainmentOperations,
files: files ?? this.files, // <-- Aggiunto
customerDisplayName: customerDisplayName ?? this.customerDisplayName,
companyId: companyId ?? this.companyId,
);
}
String? storeId,
int? quantity,
String? staffId,
String? staffDisplayName,
String? lastCampaignId,
OperationStatus? status,
String? customerId,
String? customerDisplayName,
String? reference,
List<AttachmentModel>? attachments,
}) => OperationModel(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
type: type ?? this.type,
providerId: providerId ?? this.providerId,
modelId: modelId ?? this.modelId,
description: description ?? this.description,
expirationDate: expirationDate ?? this.expirationDate,
note: note ?? this.note,
showInDashboard: showInDashboard ?? this.showInDashboard,
batchUuid: batchUuid ?? this.batchUuid,
companyId: companyId ?? this.companyId,
storeId: storeId ?? this.storeId,
quantity: quantity ?? this.quantity,
staffId: staffId ?? this.staffId,
staffDisplayName: staffDisplayName ?? this.staffDisplayName,
lastCampaignId: lastCampaignId ?? this.lastCampaignId,
status: status ?? this.status,
customerId: customerId ?? this.customerId,
customerDisplayName: customerDisplayName ?? this.customerDisplayName,
reference: reference ?? this.reference,
attachments: attachments ?? this.attachments,
);
@override
List<Object?> get props => [
id,
createdAt,
storeId,
employeeId,
customerId,
number,
isBozza,
type,
providerId,
modelId,
description,
expirationDate,
note,
resultOk,
al,
mnp,
nip,
unica,
telepass,
energyOperations,
finOperations,
entertainmentOperations,
files, // <-- Aggiunto
customerDisplayName,
showInDashboard,
batchUuid,
companyId,
storeId,
quantity,
staffId,
staffDisplayName,
lastCampaignId,
status,
customerId,
customerDisplayName,
reference,
attachments,
];
factory OperationModel.empty({required String companyId}) {
return OperationModel(id: null, createdAt: null, companyId: companyId);
}
factory OperationModel.fromMap(Map<String, dynamic> map) {
return OperationModel(
id: map['id'].toString(),
id: map['id'],
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
: DateTime.now(),
storeId: map['store_id'] ?? '',
employeeId: map['employee_id']?.toString(),
customerId: map['customer_id']?.toString(),
number: map['number']?.toString() ?? '',
isBozza: map['bozza'] ?? true,
note: map['note'] ?? '',
resultOk: map['result_ok'] ?? true,
al: map['al'] ?? 0,
mnp: map['mnp'] ?? 0,
nip: map['nip'] ?? 0,
unica: map['unica'] ?? 0,
telepass: map['telepass'] ?? 0,
// Estrazione sicura liste collegate
energyOperations:
(map['energy_operation'] as List?)
?.map((x) => EnergyOperationModel.fromMap(x))
.toList() ??
const [],
finOperations:
(map['fin_operation'] as List?)
?.map((x) => FinOperationModel.fromMap(x))
.toList() ??
const [],
entertainmentOperations:
(map['entertainment_operation'] as List?)
?.map((x) => EntertainmentOperationModel.fromMap(x))
.toList() ??
const [],
// I FILE! (Assicurati che la foreign key su Supabase usi esattamente questo nome)
files:
(map['operation_file'] as List?)
?.map((x) => OperationFileModel.fromMap(x))
.toList() ??
const [],
// Display name del cliente con fallback
customerDisplayName: map['customer'] != null
? "${map['customer']['nome'] ?? ''}".myFormat()
: "Cliente non assegnato",
: null,
type: map['type'] as String? ?? '',
providerId: map['provider_id'] as String? ?? '',
modelId: map['model_id'] as String? ?? '',
description: map['description'] as String? ?? '',
expirationDate: map['expiration_date'] != null
? DateTime.parse(map['expiration_date'])
: null,
note: map['note'] as String? ?? '',
showInDashboard: map['show_in_dashboard'] as bool,
batchUuid: map['batch_uuid'] as String,
companyId: map['company_id'] as String,
storeId: map['store_id'] as String? ?? '',
quantity: map['quantity'] is int
? map['quantity']
: int.tryParse(map['quantity']?.toString() ?? '0') ?? 0,
staffId: map['staff_id'] as String? ?? '',
lastCampaignId: map['last_campaign_id'] as String? ?? '',
status: OperationStatus.fromString(map['status']),
customerId: map['customer_id'] as String? ?? '',
reference: map['reference'] as String? ?? '',
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'store_id': storeId,
'employee_id': employeeId,
'customer_id': customerId,
'number': number,
'bozza': isBozza,
'type': type,
'provider_id': providerId,
'model_id': modelId,
'description': description,
if (expirationDate != null)
'expiration_date': expirationDate!.toIso8601String(),
'note': note,
'result_ok': resultOk,
'al': al,
'mnp': mnp,
'nip': nip,
'unica': unica,
'telepass': telepass,
'show_in_dashboard': showInDashboard,
'batch_uuid': batchUuid,
'company_id': companyId,
// Le liste non le mettiamo qui perché vanno in tabelle diverse!
'store_id': storeId,
'quantity': quantity,
if (staffId != null) 'staff_id': staffId,
if (lastCampaignId != null) 'last_campaign_id': lastCampaignId,
'status': status.supabaseName,
if (customerId != null) 'customer_id': customerId,
'reference': reference,
};
}
}