feat-ultimi_servizi-contratti_in_scadenza #12

Merged
brontomark merged 18 commits from feat-ultimi_servizi-contratti_in_scadenza into main 2026-05-04 15:36:42 +02:00
9 changed files with 358 additions and 321 deletions
Showing only changes of commit ac97e47771 - Show all commits

View File

@@ -0,0 +1,111 @@
import 'dart:typed_data';
import 'package:equatable/equatable.dart';
class AttachmentModel extends Equatable {
final String? id;
final DateTime? createdAt;
final String? customerId;
final String? operationId;
final String name;
final String extension;
final String storagePath;
final int fileSize;
final Uint8List? localBytes;
final String companyId;
const AttachmentModel({
this.id,
this.createdAt,
this.customerId,
this.operationId,
required this.name,
required this.extension,
required this.storagePath,
required this.fileSize,
this.localBytes,
required this.companyId,
});
@override
List<Object?> get props => [
id,
createdAt,
customerId,
operationId,
name,
extension,
storagePath,
fileSize,
localBytes,
companyId,
];
bool get isLocal => localBytes != null;
bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf';
String get sizeFormatted {
if (fileSize <= 0) return "0 B";
const suffixes = ["B", "KB", "MB", "GB", "TB"];
var i = (fileSize.toString().length - 1) ~/ 3;
if (i >= suffixes.length) i = suffixes.length - 1;
double num = fileSize / (1 << (i * 10));
return "${num.toStringAsFixed(i == 0 ? 0 : 1)} ${suffixes[i]}";
}
AttachmentModel copyWith({
String? id,
DateTime? createdAt,
String? customerId,
String? operationId,
String? name,
String? extension,
String? storagePath,
int? fileSize,
Uint8List? localBytes,
String? companyId,
}) => AttachmentModel(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
customerId: customerId ?? this.customerId,
operationId: operationId ?? this.operationId,
name: name ?? this.name,
extension: extension ?? this.extension,
storagePath: storagePath ?? this.storagePath,
fileSize: fileSize ?? this.fileSize,
localBytes: localBytes ?? this.localBytes,
companyId: companyId ?? this.companyId,
);
factory AttachmentModel.fromMap(Map<String, dynamic> map) {
return AttachmentModel(
id: map['id'] as String,
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
: null,
customerId: map['customer_id'] as String?,
operationId: map['operation_id'] as String?,
name: map['name'] as String,
extension: map['extension'] as String,
storagePath: map['storage_path'] as String,
fileSize: map['file_size'] is int
? map['file_size']
: int.tryParse(map['file_size']?.toString() ?? '0') ?? 0,
companyId: map['company_id'] as String,
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'name': name,
'extension': extension,
'storage_path': storagePath,
'customer_id': customerId,
'operation_id': operationId,
'file_size': fileSize,
'company_id': companyId,
};
}
}

View File

@@ -135,8 +135,8 @@ class CustomerCubit extends Cubit<CustomerState> {
String? email, String? email,
}) async { }) async {
final newCustomer = CustomerModel( final newCustomer = CustomerModel(
nome: name, name: name,
telefono: phone ?? '', phoneNumber: phone ?? '',
email: email ?? '', email: email ?? '',
companyId: _sessionCubit.state.company!.id!, companyId: _sessionCubit.state.company!.id!,
note: '', note: '',

View File

@@ -1,74 +1,74 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:flux/features/attachments/models/attachment_model.dart';
class CustomerModel extends Equatable { class CustomerModel extends Equatable {
final String? id; // Bigint in SQL final String? id; // Bigint in SQL
final DateTime? createdAt; final DateTime? createdAt;
final String nome; final String name;
final String telefono; final String phoneNumber;
final String email; final String email;
final String note; final String note;
final DateTime? dataUltimoContatto; final DateTime? lastContactDate;
final bool nonDisturbare; final bool doNotDisturb;
final String companyId; // UUID final String companyId; // UUID
final bool isActive; final bool isActive;
final List<CustomerFileModel> files; final List<AttachmentModel> attachments;
const CustomerModel({ const CustomerModel({
this.id, this.id,
this.createdAt, this.createdAt,
required this.nome, required this.name,
required this.telefono, required this.phoneNumber,
required this.email, required this.email,
required this.note, required this.note,
this.dataUltimoContatto, this.lastContactDate,
this.nonDisturbare = false, this.doNotDisturb = false,
required this.companyId, required this.companyId,
this.isActive = true, this.isActive = true,
this.files = const [], this.attachments = const [],
}); });
@override @override
List<Object?> get props => [ List<Object?> get props => [
id, id,
createdAt, createdAt,
nome, name,
telefono, phoneNumber,
email, email,
note, note,
dataUltimoContatto, lastContactDate,
nonDisturbare, doNotDisturb,
companyId, companyId,
isActive, isActive,
files, attachments,
]; ];
CustomerModel copyWith({ CustomerModel copyWith({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
String? nome, String? name,
String? telefono, String? phoneNumber,
String? email, String? email,
String? note, String? note,
DateTime? dataUltimoContatto, DateTime? lastContactDate,
bool? nonDisturbare, bool? doNotDisturb,
String? companyId, String? companyId,
bool? isActive, bool? isActive,
List<CustomerFileModel>? files, List<AttachmentModel>? attachments,
}) { }) {
return CustomerModel( return CustomerModel(
id: id ?? this.id, id: id ?? this.id,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
nome: nome ?? this.nome, name: name ?? this.name,
telefono: telefono ?? this.telefono, phoneNumber: phoneNumber ?? this.phoneNumber,
email: email ?? this.email, email: email ?? this.email,
note: note ?? this.note, note: note ?? this.note,
dataUltimoContatto: dataUltimoContatto ?? this.dataUltimoContatto, lastContactDate: lastContactDate ?? this.lastContactDate,
nonDisturbare: nonDisturbare ?? this.nonDisturbare, doNotDisturb: doNotDisturb ?? this.doNotDisturb,
companyId: companyId ?? this.companyId, companyId: companyId ?? this.companyId,
isActive: isActive ?? this.isActive, isActive: isActive ?? this.isActive,
files: files ?? this.files, attachments: attachments ?? this.attachments,
); );
} }
@@ -78,34 +78,29 @@ class CustomerModel extends Equatable {
createdAt: map['created_at'] != null createdAt: map['created_at'] != null
? DateTime.parse(map['created_at']) ? DateTime.parse(map['created_at'])
: null, : null,
nome: (map['nome'] as String).myFormat(), name: (map['name'] as String).myFormat(),
telefono: map['telefono'], phoneNumber: map['phone_number'],
email: map['email'], email: map['email'],
note: map['note'] ?? '', note: map['note'] ?? '',
dataUltimoContatto: map['data_ultimo_contatto'] != null lastContactDate: map['last_contact_date'] != null
? DateTime.parse(map['data_ultimo_contatto']) ? DateTime.parse(map['last_contact_date'])
: null, : null,
nonDisturbare: map['non_disturbare'] ?? false, doNotDisturb: map['do_not_disturb'] ?? false,
companyId: map['company_id'] as String, companyId: map['company_id'] as String,
isActive: map['is_active'] ?? true, isActive: map['is_active'] ?? true,
files:
(map['customer_file'] as List?)
?.map((x) => CustomerFileModel.fromMap(x))
.toList() ??
const [],
); );
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return { return {
if (id != null) 'id': id, if (id != null) 'id': id,
'nome': nome.toLowerCase().trim(), 'name': name.toLowerCase().trim(),
'telefono': telefono, 'phone_number': phoneNumber,
'email': email.toLowerCase().trim(), 'email': email.toLowerCase().trim(),
'note': note, 'note': note,
if (dataUltimoContatto != null) if (lastContactDate != null)
'data_ultimo_contatto': dataUltimoContatto!.toIso8601String(), 'last_contact_date': lastContactDate!.toIso8601String(),
'non_disturbare': nonDisturbare, 'do_not_disturb': doNotDisturb,
'company_id': companyId, 'company_id': companyId,
'is_active': isActive, 'is_active': isActive,
}; };

View File

@@ -62,7 +62,7 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
backgroundColor: context.background, backgroundColor: context.background,
appBar: AppBar( appBar: AppBar(
title: Text( title: Text(
widget.customer.nome, widget.customer.name,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
), ),
backgroundColor: context.background, backgroundColor: context.background,
@@ -103,7 +103,7 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_infoTile(Icons.phone_android, "Telefono", widget.customer.telefono), _infoTile(Icons.phone_android, "Telefono", widget.customer.phoneNumber),
_infoTile( _infoTile(
Icons.email_outlined, Icons.email_outlined,
"Email", "Email",
@@ -117,7 +117,7 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
: widget.customer.note, : widget.customer.note,
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
if (widget.customer.nonDisturbare) if (widget.customer.doNotDisturb)
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -191,8 +191,8 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
context: context, context: context,
builder: (context) => QrUploadDialog( builder: (context) => QrUploadDialog(
deepLinkUrl: deepLinkUrl:
'fluxapp://customer/${widget.customer.id}/upload?name=${Uri.encodeComponent(widget.customer.nome)}', 'fluxapp://customer/${widget.customer.id}/upload?name=${Uri.encodeComponent(widget.customer.name)}',
title: 'Scatta per ${widget.customer.nome}', title: 'Scatta per ${widget.customer.name}',
), ),
); );
}, },

View File

@@ -30,15 +30,15 @@ class _CustomerFormState extends State<CustomerForm> {
void initState() { void initState() {
super.initState(); super.initState();
// Se widget.customer è null, i campi saranno vuoti // Se widget.customer è null, i campi saranno vuoti
_nomeController = TextEditingController(text: widget.customer?.nome ?? ''); _nomeController = TextEditingController(text: widget.customer?.name ?? '');
_telefonoController = TextEditingController( _telefonoController = TextEditingController(
text: widget.customer?.telefono ?? '', text: widget.customer?.phoneNumber ?? '',
); );
_emailController = TextEditingController( _emailController = TextEditingController(
text: widget.customer?.email ?? '', text: widget.customer?.email ?? '',
); );
_noteController = TextEditingController(text: widget.customer?.note ?? ''); _noteController = TextEditingController(text: widget.customer?.note ?? '');
_nonDisturbare = widget.customer?.nonDisturbare ?? false; _nonDisturbare = widget.customer?.doNotDisturb ?? false;
} }
@override @override
@@ -56,19 +56,19 @@ class _CustomerFormState extends State<CustomerForm> {
// o creandone uno da zero, preservando l'ID in caso di modifica. // o creandone uno da zero, preservando l'ID in caso di modifica.
final updatedCustomer = final updatedCustomer =
widget.customer?.copyWith( widget.customer?.copyWith(
nome: _nomeController.text.trim(), name: _nomeController.text.trim(),
telefono: _telefonoController.text.trim(), phoneNumber: _telefonoController.text.trim(),
email: _emailController.text.trim(), email: _emailController.text.trim(),
note: _noteController.text.trim(), note: _noteController.text.trim(),
nonDisturbare: _nonDisturbare, doNotDisturb: _nonDisturbare,
) ?? ) ??
CustomerModel( CustomerModel(
// Caso nuovo cliente // Caso nuovo cliente
nome: _nomeController.text.trim(), name: _nomeController.text.trim(),
telefono: _telefonoController.text.trim(), phoneNumber: _telefonoController.text.trim(),
email: _emailController.text.trim(), email: _emailController.text.trim(),
note: _noteController.text.trim(), note: _noteController.text.trim(),
nonDisturbare: _nonDisturbare, doNotDisturb: _nonDisturbare,
companyId: '', // Verrà iniettato dal Bloc o dal chiamante companyId: '', // Verrà iniettato dal Bloc o dal chiamante
); );

View File

@@ -103,7 +103,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
if (nuovoCliente != null) { if (nuovoCliente != null) {
operationsCubit.updateField( operationsCubit.updateField(
customerId: nuovoCliente.id, customerId: nuovoCliente.id,
customerDisplayName: nuovoCliente.nome, customerDisplayName: nuovoCliente.name,
); );
setState(() { setState(() {
@@ -151,7 +151,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
final customer = state.customers[index]; final customer = state.customers[index];
// Assumo che il tuo CustomerModel abbia le proprietà name e surname. // Assumo che il tuo CustomerModel abbia le proprietà name e surname.
// Adatta queste variabili al tuo modello reale! // Adatta queste variabili al tuo modello reale!
final displayName = customer.nome.trim(); final displayName = customer.name.trim();
return ListTile( return ListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,

View File

@@ -166,7 +166,7 @@ class _CustomerTile extends StatelessWidget {
radius: 24, radius: 24,
backgroundColor: context.accent.withValues(alpha: 0.1), backgroundColor: context.accent.withValues(alpha: 0.1),
child: Text( child: Text(
customer.nome.isNotEmpty ? customer.nome[0].toUpperCase() : '?', customer.name.isNotEmpty ? customer.name[0].toUpperCase() : '?',
style: TextStyle( style: TextStyle(
color: context.accent, color: context.accent,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -174,7 +174,7 @@ class _CustomerTile extends StatelessWidget {
), ),
), ),
title: Text( title: Text(
customer.nome, customer.name,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
), ),
subtitle: Padding( subtitle: Padding(
@@ -184,7 +184,7 @@ class _CustomerTile extends StatelessWidget {
Icon(Icons.phone_android, size: 14, color: context.secondaryText), Icon(Icons.phone_android, size: 14, color: context.secondaryText),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
customer.telefono, customer.phoneNumber,
style: TextStyle(color: context.secondaryText), style: TextStyle(color: context.secondaryText),
), ),
if (customer.email.isNotEmpty) ...[ if (customer.email.isNotEmpty) ...[

View File

@@ -2,6 +2,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/extensions.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/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:flux/features/operations/models/operation_file_model.dart'; import 'package:flux/features/operations/models/operation_file_model.dart';
@@ -21,11 +22,8 @@ class OperationsRepository {
.from('operation') .from('operation')
.select(''' .select('''
*, *,
customer(nome), customer(name),
energy_operation(*), staff_member(name)
fin_operation(*),
entertainment_operation(*),
operation_file(*)
''') ''')
.eq('id', id) .eq('id', id)
.single(); .single();
@@ -45,16 +43,13 @@ class OperationsRepository {
DateTimeRange? dateRange, DateTimeRange? dateRange,
}) async { }) async {
try { try {
// Nota: 'customer(name, surname)' serve per il display name nella card
var query = _supabase var query = _supabase
.from('operation') .from('operation')
.select(''' .select('''
*, *,
customer(nome), customer(name),
energy_operation(*), staff_member(name),
fin_operation(*), attachments(*)
entertainment_operation(*),
operation_file(*)
''') ''')
.eq('company_id', companyId); .eq('company_id', companyId);
@@ -68,7 +63,7 @@ class OperationsRepository {
if (searchTerm != null && searchTerm.isNotEmpty) { if (searchTerm != null && searchTerm.isNotEmpty) {
// Filtra sui campi della tabella principale O su quelli della tabella joinata // Filtra sui campi della tabella principale O su quelli della tabella joinata
query = query.or( 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)) .map((map) => OperationModel.fromMap(map))
.toList(); .toList();
} catch (e) { } catch (e) {
throw Exception('Errore nel caricamento servizi: $e'); throw Exception('$e');
} }
} }
@@ -112,66 +107,9 @@ class OperationsRepository {
final String newId = operationData['id']; 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) // 4. UPLOAD DEI FILE LOCALI (Nuovi)
// Filtriamo solo i file che non hanno ancora un ID (quindi sono locali) // 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) .where((f) => f.id == null)
.toList(); .toList();
@@ -202,7 +140,7 @@ class OperationsRepository {
); );
// B. Inserimento riga nel DB relazionale // B. Inserimento riga nel DB relazionale
await _supabase.from('operation_file').insert(fileToSave.toMap()); await _supabase.from('attachment').insert(fileToSave.toMap());
} }
uploadTasks.add(uploadAndLink()); uploadTasks.add(uploadAndLink());
@@ -219,10 +157,9 @@ class OperationsRepository {
.from('operation') .from('operation')
.select(''' .select('''
*, *,
energy_operation(*), staff_member(name),
fin_operation(*), customer(name),
entertainment_operation(*), attachments(*)
operation_file(*)
''') ''')
.eq('id', newId) .eq('id', newId)
.single(); .single();
@@ -230,7 +167,7 @@ class OperationsRepository {
return OperationModel.fromMap(updatedOperationData); return OperationModel.fromMap(updatedOperationData);
} catch (e) { } catch (e) {
// Qui potresti aggiungere una logica di "rollback manuale" se necessario // 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 { try {
await _supabase.from('operation').delete().eq('id', id); await _supabase.from('operation').delete().eq('id', id);
} catch (e) { } 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 // Cerchiamo i tipi più frequenti associati ai servizi di questa company
// Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id // Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id
final response = await _supabase final response = await _supabase
.from('entertainment_operation') .from('operation')
.select('type, operation!inner(store!inner(company_id))') .select('description')
.eq('operation.store.company_id', companyId) .eq('company_id', companyId)
.limit(100); // Prendiamo un campione .eq('type', 'Entertainment')
.limit(50); // Prendiamo un campione
// Logica rapida per contare le occorrenze e prendere i primi 5 // Logica rapida per contare le occorrenze e prendere i primi 5
final Map<String, int> counts = {}; final Map<String, int> counts = {};
for (var item in (response as List)) { for (var item in (response as List)) {
final type = item['type'] as String; final description = item['description'] as String;
counts[type] = (counts[type] ?? 0) + 1; counts[description] = (counts[description] ?? 0) + 1;
} }
var sortedKeys = counts.keys.toList() var sortedKeys = counts.keys.toList()
@@ -276,19 +214,19 @@ class OperationsRepository {
} }
/// Ascolta in tempo reale i file caricati per una pratica /// Ascolta in tempo reale i file caricati per una pratica
Stream<List<OperationFileModel>> getOperationFilesStream(String operationId) { Stream<List<AttachmentModel>> getOperationFilesStream(String operationId) {
return _supabase return _supabase
.from('operation_file') .from('attachment')
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.eq('operation_id', operationId) .eq('operation_id', operationId)
.order('created_at', ascending: false) .order('created_at', ascending: false)
.map( .map(
(listOfMaps) => (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 String operationId,
required PlatformFile pickedFile, required PlatformFile pickedFile,
}) async { }) async {
@@ -299,7 +237,8 @@ class OperationsRepository {
final storagePath = final storagePath =
'$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; '$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
final int fileSize = pickedFile.size; final int fileSize = pickedFile.size;
final fileToSave = OperationFileModel( final fileToSave = AttachmentModel(
companyId: GetIt.I.get<SessionCubit>().state.company!.id!,
operationId: operationId, operationId: operationId,
name: cleanFileName.fileNameWithoutExtension(), name: cleanFileName.fileNameWithoutExtension(),
extension: cleanFileName.fileExtension(), extension: cleanFileName.fileExtension(),
@@ -327,12 +266,12 @@ class OperationsRepository {
} }
final response = await _supabase final response = await _supabase
.from('operation_file') .from('attachment')
.insert(fileToSave.toMap()) .insert(fileToSave.toMap())
.select() .select()
.single(); .single();
return OperationFileModel.fromMap(response); return AttachmentModel.fromMap(response);
} catch (e) { } catch (e) {
throw 'Errore durante l\'upload: $e'; throw 'Errore durante l\'upload: $e';
} }
@@ -342,17 +281,13 @@ class OperationsRepository {
required OperationFileModel file, required OperationFileModel file,
required String customerId, required String customerId,
}) async { }) async {
CustomerFileModel fileToCopy = CustomerFileModel( await _supabase
customerId: customerId, .from('attachment')
name: file.name, .update({'customer_id': customerId})
storagePath: file.storagePath, .eq('id', file.id!);
extension: file.extension,
fileSize: file.fileSize,
);
await _customerRepository.saveFileReference(fileToCopy);
} }
Future<void> deleteOperationFiles(List<OperationFileModel> files) async { Future<void> deleteOperationFiles(List<AttachmentModel> files) async {
if (files.isEmpty) return; if (files.isEmpty) return;
// 1. Prepariamo le liste di ID e di Percorsi // 1. Prepariamo le liste di ID e di Percorsi
final List<String> idsToDelete = files.map((f) => f.id!).toList(); final List<String> idsToDelete = files.map((f) => f.id!).toList();
@@ -360,18 +295,14 @@ class OperationsRepository {
try { try {
await _supabase await _supabase
.from('operation_file') .from('attachment')
.delete() .update({'operation_id': null})
.inFilter('id', idsToDelete); .inFilter('id', idsToDelete);
await _supabase.storage.from('documents').remove(storagePaths); await _supabase.storage.from('documents').remove(storagePaths);
debugPrint("Eliminati con successo ${files.length} file.");
} on PostgrestException catch (e) { } on PostgrestException catch (e) {
debugPrint("Errore DB: ${e.message}");
throw 'Errore database: ${e.message}'; throw 'Errore database: ${e.message}';
} catch (e) { } catch (e) {
debugPrint("Errore generico: $e");
throw 'Errore durante l\'eliminazione dei file: $e'; throw 'Errore durante l\'eliminazione dei file: $e';
} }
} }

View File

@@ -1,200 +1,200 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/attachments/models/attachment_model.dart';
import 'package:flux/features/operations/models/energy_operation_model.dart';
import 'package:flux/features/operations/models/entertainment_operation_model.dart'; enum OperationStatus {
import 'package:flux/features/operations/models/fin_operation_model.dart'; ok('ok'),
import 'package:flux/features/operations/models/operation_file_model.dart'; // <-- Aggiunto Import 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 { class OperationModel extends Equatable {
final String? id; final String? id;
final DateTime? createdAt; final DateTime? createdAt;
final String storeId; final String type;
final String? employeeId; final String? providerId;
final String? customerId; final String? modelId;
final String number; final String? description;
final bool isBozza; final DateTime? expirationDate;
final String note; final String note;
final bool resultOk; final bool showInDashboard;
final String? customerDisplayName; final String batchUuid;
final String companyId; final String companyId;
final String storeId;
// Telefonia final int quantity;
final int al; final String? staffId;
final int mnp; final String staffDisplayName;
final int nip; final String? lastCampaignId;
final int unica; final OperationStatus status;
final int telepass; final String? customerId;
final String customerDisplayName;
// Moduli (Liste) final String reference;
final List<EnergyOperationModel> energyOperations;
final List<FinOperationModel> finOperations;
final List<EntertainmentOperationModel> entertainmentOperations;
// ALLEGATI (Aggiunto) // ALLEGATI (Aggiunto)
final List<OperationFileModel> files; final List<AttachmentModel> attachments;
const OperationModel({ const OperationModel({
this.id, this.id,
this.createdAt, this.createdAt,
required this.storeId, this.type = '',
this.employeeId, this.providerId,
this.customerId, this.modelId,
required this.number, this.description,
this.isBozza = true, this.expirationDate,
this.note = '', this.note = '',
this.resultOk = true, this.showInDashboard = true,
this.al = 0, this.batchUuid = '',
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,
required this.companyId, 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({ OperationModel copyWith({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
String? storeId, String? type,
String? employeeId, String? providerId,
String? customerId, String? modelId,
String? number, String? description,
bool? isBozza, DateTime? expirationDate,
String? note, String? note,
bool? resultOk, bool? showInDashboard,
int? al, String? batchUuid,
int? mnp,
int? nip,
int? unica,
int? telepass,
List<EnergyOperationModel>? energyOperations,
List<FinOperationModel>? finOperations,
List<EntertainmentOperationModel>? entertainmentOperations,
List<OperationFileModel>? files, // <-- Aggiunto
String? customerDisplayName,
String? companyId, String? companyId,
}) { String? storeId,
return OperationModel( int? quantity,
id: id ?? this.id, String? staffId,
createdAt: createdAt ?? this.createdAt, String? staffDisplayName,
storeId: storeId ?? this.storeId, String? lastCampaignId,
employeeId: employeeId ?? this.employeeId, OperationStatus? status,
customerId: customerId ?? this.customerId, String? customerId,
number: number ?? this.number, String? customerDisplayName,
isBozza: isBozza ?? this.isBozza, String? reference,
note: note ?? this.note, List<AttachmentModel>? attachments,
resultOk: resultOk ?? this.resultOk, }) => OperationModel(
al: al ?? this.al, id: id ?? this.id,
mnp: mnp ?? this.mnp, createdAt: createdAt ?? this.createdAt,
nip: nip ?? this.nip, type: type ?? this.type,
unica: unica ?? this.unica, providerId: providerId ?? this.providerId,
telepass: telepass ?? this.telepass, modelId: modelId ?? this.modelId,
energyOperations: energyOperations ?? this.energyOperations, description: description ?? this.description,
finOperations: finOperations ?? this.finOperations, expirationDate: expirationDate ?? this.expirationDate,
entertainmentOperations: note: note ?? this.note,
entertainmentOperations ?? this.entertainmentOperations, showInDashboard: showInDashboard ?? this.showInDashboard,
files: files ?? this.files, // <-- Aggiunto batchUuid: batchUuid ?? this.batchUuid,
customerDisplayName: customerDisplayName ?? this.customerDisplayName, companyId: companyId ?? this.companyId,
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 @override
List<Object?> get props => [ List<Object?> get props => [
id, id,
createdAt, createdAt,
storeId, type,
employeeId, providerId,
customerId, modelId,
number, description,
isBozza, expirationDate,
note, note,
resultOk, showInDashboard,
al, batchUuid,
mnp,
nip,
unica,
telepass,
energyOperations,
finOperations,
entertainmentOperations,
files, // <-- Aggiunto
customerDisplayName,
companyId, 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) { factory OperationModel.fromMap(Map<String, dynamic> map) {
return OperationModel( return OperationModel(
id: map['id'].toString(), id: map['id'],
createdAt: map['created_at'] != null createdAt: map['created_at'] != null
? DateTime.parse(map['created_at']) ? DateTime.parse(map['created_at'])
: DateTime.now(), : null,
storeId: map['store_id'] ?? '', type: map['type'] as String? ?? '',
employeeId: map['employee_id']?.toString(), providerId: map['provider_id'] as String? ?? '',
customerId: map['customer_id']?.toString(), modelId: map['model_id'] as String? ?? '',
number: map['number']?.toString() ?? '', description: map['description'] as String? ?? '',
isBozza: map['bozza'] ?? true, expirationDate: map['expiration_date'] != null
note: map['note'] ?? '', ? DateTime.parse(map['expiration_date'])
resultOk: map['result_ok'] ?? true, : null,
al: map['al'] ?? 0, note: map['note'] as String? ?? '',
mnp: map['mnp'] ?? 0, showInDashboard: map['show_in_dashboard'] as bool,
nip: map['nip'] ?? 0, batchUuid: map['batch_uuid'] as String,
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",
companyId: map['company_id'] 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() { Map<String, dynamic> toMap() {
return { return {
if (id != null) 'id': id, if (id != null) 'id': id,
'store_id': storeId, 'type': type,
'employee_id': employeeId, 'provider_id': providerId,
'customer_id': customerId, 'model_id': modelId,
'number': number, 'description': description,
'bozza': isBozza, if (expirationDate != null)
'expiration_date': expirationDate!.toIso8601String(),
'note': note, 'note': note,
'result_ok': resultOk, 'show_in_dashboard': showInDashboard,
'al': al, 'batch_uuid': batchUuid,
'mnp': mnp,
'nip': nip,
'unica': unica,
'telepass': telepass,
'company_id': companyId, '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,
}; };
} }
} }