feature aggiunta
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flux/features/auth/ui/auth_screen.dart';
|
||||
import 'package:flux/features/company/ui/create_company_screen.dart';
|
||||
@@ -8,7 +7,6 @@ import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
||||
import 'package:flux/features/home/ui/home_screen.dart';
|
||||
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
||||
import 'package:flux/features/master_data/store/ui/create_store_screen.dart';
|
||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||
import 'package:flux/features/services/models/service_model.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -19,4 +19,24 @@ extension MyStringExtensions on String? {
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
String fileExtension() {
|
||||
if (this == null || this!.trim().isEmpty) return '';
|
||||
|
||||
final parts = this!.split('.');
|
||||
if (parts.length < 2) return ''; // Nessuna estensione trovata
|
||||
|
||||
return parts.last.toLowerCase();
|
||||
}
|
||||
|
||||
String fileNameWithoutExtension() {
|
||||
if (this == null || this!.trim().isEmpty) return '';
|
||||
|
||||
final parts = this!.split('.');
|
||||
if (parts.length < 2) return this!; // Nessuna estensione trovata
|
||||
|
||||
return parts
|
||||
.sublist(0, parts.length - 1)
|
||||
.join('.'); // Ritorna tutto tranne l'ultima parte
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flux/core/utils/string_extensions.dart';
|
||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../models/customer_model.dart';
|
||||
|
||||
class CustomerRepository {
|
||||
final SupabaseClient _client = GetIt.I<SupabaseClient>();
|
||||
final SupabaseClient _supabase = GetIt.I<SupabaseClient>();
|
||||
final String companyId = GetIt.I.get<SessionBloc>().state.company!.id;
|
||||
|
||||
// Crea un nuovo cliente
|
||||
Future<CustomerModel> saveCustomer(CustomerModel customer) async {
|
||||
try {
|
||||
final response = await _client
|
||||
final response = await _supabase
|
||||
.from('customer')
|
||||
.upsert(customer.toJson())
|
||||
.select()
|
||||
@@ -25,7 +26,7 @@ class CustomerRepository {
|
||||
|
||||
Future<CustomerModel> updateCustomer(CustomerModel customer) async {
|
||||
try {
|
||||
final response = await _client
|
||||
final response = await _supabase
|
||||
.from('customer')
|
||||
.update(customer.toJson())
|
||||
.eq('id', customer.id!)
|
||||
@@ -40,7 +41,7 @@ class CustomerRepository {
|
||||
// Recupera tutti i clienti dell'azienda
|
||||
Future<List<CustomerModel>> getCustomers(String companyId) async {
|
||||
try {
|
||||
final response = await _client
|
||||
final response = await _supabase
|
||||
.from('customer')
|
||||
.select('*, customer_file(count)')
|
||||
.eq('company_id', companyId)
|
||||
@@ -59,7 +60,7 @@ class CustomerRepository {
|
||||
String query,
|
||||
) async {
|
||||
try {
|
||||
final response = await _client
|
||||
final response = await _supabase
|
||||
.from('customer')
|
||||
.select()
|
||||
.eq('company_id', companyId)
|
||||
@@ -75,13 +76,13 @@ class CustomerRepository {
|
||||
/// Recupera i file di un cliente specifico
|
||||
Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async {
|
||||
try {
|
||||
final response = await _client
|
||||
final response = await _supabase
|
||||
.from('customer_file')
|
||||
.select()
|
||||
.eq('customer_id', customerId);
|
||||
|
||||
return (response as List)
|
||||
.map((f) => CustomerFileModel.fromJson(f))
|
||||
.map((f) => CustomerFileModel.fromMap(f))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw 'Errore recupero file: $e';
|
||||
@@ -90,7 +91,7 @@ class CustomerRepository {
|
||||
|
||||
/// Salva il riferimento del file nel DB
|
||||
Future<void> saveFileReference(CustomerFileModel file) async {
|
||||
await _client.from('customer_file').insert(file.toJson());
|
||||
await _supabase.from('customer_file').insert(file.toMap());
|
||||
}
|
||||
|
||||
/// Carica un file e salva il riferimento nel database
|
||||
@@ -98,15 +99,24 @@ class CustomerRepository {
|
||||
required String customerId,
|
||||
required PlatformFile pickedFile,
|
||||
}) async {
|
||||
final cleanFileName = pickedFile.name.replaceAll(
|
||||
RegExp(r'[^a-zA-Z0-9\.\-]'),
|
||||
'_',
|
||||
);
|
||||
final storagePath =
|
||||
'$companyId/customers/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
|
||||
final int fileSize = pickedFile.size;
|
||||
final fileToSave = CustomerFileModel(
|
||||
customerId: customerId,
|
||||
name: cleanFileName.fileNameWithoutExtension(),
|
||||
extension: cleanFileName.fileExtension(),
|
||||
url: '',
|
||||
fileSize: fileSize,
|
||||
);
|
||||
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
|
||||
? 'application/pdf'
|
||||
: 'image/${fileToSave.extension}';
|
||||
try {
|
||||
final user = _client.auth.currentUser;
|
||||
if (user == null) throw 'Utente non autenticato';
|
||||
|
||||
final fileName = pickedFile.name;
|
||||
final extension = pickedFile.extension ?? '';
|
||||
final path =
|
||||
'${user.id}/$customerId/${DateTime.now().millisecondsSinceEpoch}_$fileName';
|
||||
|
||||
// Usiamo bytes invece del path per massima compatibilità
|
||||
if (pickedFile.bytes == null && pickedFile.path == null) {
|
||||
throw 'Impossibile leggere il contenuto del file';
|
||||
@@ -114,32 +124,26 @@ class CustomerRepository {
|
||||
|
||||
// Se siamo su desktop/mobile abbiamo il path, su web abbiamo i bytes
|
||||
if (pickedFile.bytes != null) {
|
||||
await _client.storage
|
||||
await _supabase.storage
|
||||
.from('documents')
|
||||
.uploadBinary(path, pickedFile.bytes!);
|
||||
} else {
|
||||
final file = File(pickedFile.path!);
|
||||
await _client.storage.from('documents').upload(path, file);
|
||||
.uploadBinary(
|
||||
storagePath,
|
||||
pickedFile.bytes!,
|
||||
fileOptions: FileOptions(contentType: mimeType, upsert: true),
|
||||
);
|
||||
}
|
||||
|
||||
final String publicUrl = _client.storage
|
||||
final String publicUrl = _supabase.storage
|
||||
.from('documents')
|
||||
.getPublicUrl(path);
|
||||
.getPublicUrl(storagePath);
|
||||
|
||||
final fileRecord = CustomerFileModel(
|
||||
customerId: customerId,
|
||||
name: fileName,
|
||||
url: publicUrl,
|
||||
extension: extension,
|
||||
);
|
||||
|
||||
final response = await _client
|
||||
final response = await _supabase
|
||||
.from('customer_file')
|
||||
.insert(fileRecord.toJson())
|
||||
.insert(fileToSave.copyWith(url: publicUrl).toMap())
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return CustomerFileModel.fromJson(response);
|
||||
return CustomerFileModel.fromMap(response);
|
||||
} catch (e) {
|
||||
throw 'Errore durante l\'upload: $e';
|
||||
}
|
||||
@@ -147,13 +151,16 @@ class CustomerRepository {
|
||||
|
||||
/// Aggiorna la lista degli URL nel database
|
||||
Future<void> updateCustomerDocuments(int id, List<String> urls) async {
|
||||
await _client.from('customer').update({'document_urls': urls}).eq('id', id);
|
||||
await _supabase
|
||||
.from('customer')
|
||||
.update({'document_urls': urls})
|
||||
.eq('id', id);
|
||||
}
|
||||
|
||||
/// Elimina un file dallo storage
|
||||
Future<void> deleteDocument(String fullPath) async {
|
||||
// Il path dovrebbe essere ricavato dall'URL
|
||||
final path = fullPath.split('documents/').last;
|
||||
await _client.storage.from('documents').remove([path]);
|
||||
await _supabase.storage.from('documents').remove([path]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ class CustomerFileModel extends Equatable {
|
||||
final String url;
|
||||
final String extension;
|
||||
final DateTime? createdAt;
|
||||
final int fileSize;
|
||||
|
||||
const CustomerFileModel({
|
||||
this.id,
|
||||
@@ -15,31 +16,76 @@ class CustomerFileModel extends Equatable {
|
||||
required this.url,
|
||||
required this.extension,
|
||||
this.createdAt,
|
||||
required this.fileSize,
|
||||
});
|
||||
|
||||
factory CustomerFileModel.fromJson(Map<String, dynamic> json) {
|
||||
// Trasforma i byte in qualcosa di leggibile (KB, MB, GB)
|
||||
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]}";
|
||||
}
|
||||
|
||||
bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf';
|
||||
|
||||
CustomerFileModel copyWith({
|
||||
String? id,
|
||||
String? customerId,
|
||||
String? name,
|
||||
String? url,
|
||||
String? extension,
|
||||
DateTime? createdAt,
|
||||
int? fileSize,
|
||||
}) {
|
||||
return CustomerFileModel(
|
||||
id: json['id'] as String,
|
||||
customerId: json['customer_id'],
|
||||
name: json['name'],
|
||||
url: json['url'],
|
||||
extension: json['extension'] ?? '',
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
id: id ?? this.id,
|
||||
customerId: customerId ?? this.customerId,
|
||||
name: name ?? this.name,
|
||||
url: url ?? this.url,
|
||||
extension: extension ?? this.extension,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
fileSize: fileSize ?? this.fileSize,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
factory CustomerFileModel.fromMap(Map<String, dynamic> map) {
|
||||
return CustomerFileModel(
|
||||
id: map['id'] as String,
|
||||
customerId: map['customer_id'],
|
||||
name: map['name'],
|
||||
url: map['url'],
|
||||
extension: map['extension'] ?? '',
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
: null,
|
||||
fileSize: map['file_size'] is int
|
||||
? map['file_size']
|
||||
: int.tryParse(map['file_size']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
'customer_id': customerId,
|
||||
'name': name,
|
||||
'url': url,
|
||||
'extension': extension,
|
||||
'file_size': fileSize,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, customerId, name, url, extension, createdAt];
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
customerId,
|
||||
name,
|
||||
url,
|
||||
extension,
|
||||
createdAt,
|
||||
fileSize,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
@@ -129,6 +130,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
companyId: _sessionBloc.state.company!.id,
|
||||
),
|
||||
status: ServicesStatus.ready,
|
||||
localAttachments: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -208,7 +210,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
final serviceToSave = state.currentService!.copyWith(isBozza: isBozza);
|
||||
|
||||
// 2. Salvataggio corazzato
|
||||
await _repository.saveFullService(serviceToSave);
|
||||
await _repository.saveFullService(serviceToSave, state.localAttachments);
|
||||
|
||||
// 3. Reset e ricaricamento
|
||||
emit(state.copyWith(status: ServicesStatus.saved, currentService: null));
|
||||
@@ -222,4 +224,18 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- GESTIONE ALLEGATI LOCALI ---
|
||||
|
||||
void addAttachments(List<PlatformFile> files) {
|
||||
// Aggiungiamo i nuovi file a quelli già presenti in memoria
|
||||
final updatedList = [...state.localAttachments, ...files];
|
||||
emit(state.copyWith(localAttachments: updatedList));
|
||||
}
|
||||
|
||||
void removeLocalAttachment(int index) {
|
||||
final updatedList = List<PlatformFile>.from(state.localAttachments);
|
||||
updatedList.removeAt(index);
|
||||
emit(state.copyWith(localAttachments: updatedList));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ class ServicesState extends Equatable {
|
||||
final String query;
|
||||
final DateTimeRange? dateRange;
|
||||
final bool hasReachedMax;
|
||||
final List<PlatformFile> localAttachments;
|
||||
|
||||
const ServicesState({
|
||||
required this.status,
|
||||
@@ -19,6 +20,7 @@ class ServicesState extends Equatable {
|
||||
this.query = '',
|
||||
this.dateRange,
|
||||
this.hasReachedMax = false,
|
||||
this.localAttachments = const [],
|
||||
});
|
||||
|
||||
ServicesState copyWith({
|
||||
@@ -29,6 +31,7 @@ class ServicesState extends Equatable {
|
||||
String? query,
|
||||
DateTimeRange? dateRange,
|
||||
bool? hasReachedMax,
|
||||
List<PlatformFile>? localAttachments,
|
||||
}) {
|
||||
return ServicesState(
|
||||
status: status ?? this.status,
|
||||
@@ -38,6 +41,7 @@ class ServicesState extends Equatable {
|
||||
query: query ?? this.query,
|
||||
dateRange: dateRange ?? this.dateRange,
|
||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||
localAttachments: localAttachments ?? this.localAttachments,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,5 +54,6 @@ class ServicesState extends Equatable {
|
||||
query,
|
||||
dateRange,
|
||||
hasReachedMax,
|
||||
localAttachments,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||
import 'package:flux/core/utils/string_extensions.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;
|
||||
|
||||
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
|
||||
Future<ServiceModel> fetchServiceById(String id) async {
|
||||
@@ -16,7 +21,8 @@ class ServicesRepository {
|
||||
customer(nome),
|
||||
energy_service(*),
|
||||
fin_service(*),
|
||||
entertainment_service(*)
|
||||
entertainment_service(*),
|
||||
service_file(*)
|
||||
''')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
@@ -44,7 +50,8 @@ class ServicesRepository {
|
||||
customer(nome),
|
||||
energy_service(*),
|
||||
fin_service(*),
|
||||
entertainment_service(*)
|
||||
entertainment_service(*),
|
||||
service_file(*)
|
||||
''')
|
||||
.eq('company_id', companyId);
|
||||
|
||||
@@ -75,7 +82,10 @@ class ServicesRepository {
|
||||
}
|
||||
|
||||
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
||||
Future<void> saveFullService(ServiceModel service) async {
|
||||
Future<void> saveFullService(
|
||||
ServiceModel service,
|
||||
List<PlatformFile> localFiles,
|
||||
) async {
|
||||
try {
|
||||
// 1. Upsert del record principale
|
||||
final serviceData = await _supabase
|
||||
@@ -142,6 +152,63 @@ class ServicesRepository {
|
||||
if (insertTasks.isNotEmpty) {
|
||||
await Future.wait(insertTasks);
|
||||
}
|
||||
if (localFiles.isNotEmpty) {
|
||||
final List<Future> uploadTasks = [];
|
||||
|
||||
for (var file in localFiles) {
|
||||
// Puliamo il nome del file per evitare problemi con spazi o caratteri strani
|
||||
final cleanFileName = file.name.replaceAll(
|
||||
RegExp(r'[^a-zA-Z0-9\.\-]'),
|
||||
'_',
|
||||
);
|
||||
final storagePath =
|
||||
'$companyId/services/$newId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
|
||||
|
||||
final int fileSize = file.size;
|
||||
|
||||
final fileToSave = ServiceFileModel(
|
||||
name: cleanFileName.fileNameWithoutExtension(),
|
||||
extension: cleanFileName.fileExtension(),
|
||||
url: '',
|
||||
serviceId: newId,
|
||||
fileSize: fileSize,
|
||||
);
|
||||
|
||||
// Creiamo una funzione asincrona per caricare file e scrivere nel DB
|
||||
Future<void> uploadAndLink() async {
|
||||
// Determiniamo il MIME type corretto in base all'estensione
|
||||
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
|
||||
? 'application/pdf'
|
||||
: 'image/${fileToSave.extension}';
|
||||
// A. Upload nel Bucket Storage (usiamo i bytes così funziona anche su Web!)
|
||||
await _supabase.storage
|
||||
.from('documents')
|
||||
.uploadBinary(
|
||||
storagePath,
|
||||
file.bytes!,
|
||||
fileOptions: FileOptions(
|
||||
contentType:
|
||||
mimeType, // Diciamo a Supabase esattamente cos'è!
|
||||
upsert:
|
||||
true, // Opzionale: sovrascrive se esiste già un file con lo stesso nome
|
||||
),
|
||||
);
|
||||
|
||||
// B. Otteniamo l'URL pubblico e scriviamo il record del file nel DB
|
||||
final String publicUrl = _supabase.storage
|
||||
.from('documents')
|
||||
.getPublicUrl(storagePath);
|
||||
await _supabase
|
||||
.from('service_file')
|
||||
.insert(fileToSave.copyWith(url: publicUrl).toMap());
|
||||
}
|
||||
|
||||
uploadTasks.add(uploadAndLink());
|
||||
}
|
||||
|
||||
// Eseguiamo tutti gli upload in parallelo per la massima velocità
|
||||
await Future.wait(uploadTasks);
|
||||
}
|
||||
} catch (e) {
|
||||
// Qui potresti aggiungere una logica di "rollback manuale" se necessario
|
||||
throw Exception('Errore durante il salvataggio corazzato: $e');
|
||||
@@ -188,28 +255,4 @@ class ServicesRepository {
|
||||
]; // Fallback se non c'è ancora storia
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> uploadAttachment({
|
||||
required String serviceId,
|
||||
required String fileName,
|
||||
required Uint8List fileBytes,
|
||||
}) async {
|
||||
try {
|
||||
// 1. Upload fisico nel bucket 'service_documents'
|
||||
final path = '$serviceId/$fileName';
|
||||
await _supabase.storage
|
||||
.from('service_documents')
|
||||
.uploadBinary(path, fileBytes);
|
||||
|
||||
// 2. Registriamo l'esistenza del file nel database
|
||||
await _supabase.from('service_attachment').insert({
|
||||
'service_id': serviceId,
|
||||
'file_path': path,
|
||||
'file_name': fileName,
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
} catch (e) {
|
||||
throw "Errore upload: $e";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
lib/features/services/models/service_file_model.dart
Normal file
91
lib/features/services/models/service_file_model.dart
Normal file
@@ -0,0 +1,91 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class ServiceFileModel extends Equatable {
|
||||
final String? id;
|
||||
final DateTime? createdAt;
|
||||
final String name;
|
||||
final String extension;
|
||||
final String url;
|
||||
final String serviceId;
|
||||
final int fileSize; // <--- Aggiunto
|
||||
|
||||
const ServiceFileModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
required this.name,
|
||||
required this.extension,
|
||||
required this.url,
|
||||
required this.serviceId,
|
||||
required this.fileSize,
|
||||
});
|
||||
|
||||
// Trasforma i byte in qualcosa di leggibile (KB, MB, GB)
|
||||
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]}";
|
||||
}
|
||||
|
||||
bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf';
|
||||
|
||||
ServiceFileModel copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
String? name,
|
||||
String? extension,
|
||||
String? url,
|
||||
String? serviceId,
|
||||
int? fileSize,
|
||||
}) {
|
||||
return ServiceFileModel(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
name: name ?? this.name,
|
||||
extension: extension ?? this.extension,
|
||||
url: url ?? this.url,
|
||||
serviceId: serviceId ?? this.serviceId,
|
||||
fileSize: fileSize ?? this.fileSize,
|
||||
);
|
||||
}
|
||||
|
||||
factory ServiceFileModel.fromMap(Map<String, dynamic> map) {
|
||||
return ServiceFileModel(
|
||||
id: map['id'] as String,
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
: null,
|
||||
name: map['name'] ?? '',
|
||||
extension: map['extension'] ?? '',
|
||||
url: map['url'] ?? '',
|
||||
serviceId: map['service_id']?.toString() ?? '',
|
||||
fileSize: map['file_size'] is int
|
||||
? map['file_size']
|
||||
: int.tryParse(map['file_size']?.toString() ?? '0') ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
'name': name,
|
||||
'extension': extension,
|
||||
'url': url,
|
||||
'service_id': serviceId,
|
||||
'file_size': fileSize,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
createdAt,
|
||||
name,
|
||||
extension,
|
||||
url,
|
||||
serviceId,
|
||||
fileSize,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||
|
||||
class AttachmentsSection extends StatelessWidget {
|
||||
const AttachmentsSection({super.key});
|
||||
|
||||
Future<void> _pickFiles(BuildContext context) async {
|
||||
// Usiamo withData: true fondamentale per avere i bytes e caricare su Supabase Storage
|
||||
FilePickerResult? result = await FilePicker.pickFiles(
|
||||
allowMultiple: true,
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf', 'jpg', 'jpeg', 'png'],
|
||||
withData: true,
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
context.read<ServicesCubit>().addAttachments(result.files);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ServicesCubit, ServicesState>(
|
||||
builder: (context, state) {
|
||||
final localFiles = state.localAttachments;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"DOCUMENTI ALLEGATI",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.attach_file),
|
||||
label: const Text("Aggiungi File"),
|
||||
onPressed: () => _pickFiles(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
if (localFiles.isEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade300,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.grey.shade50,
|
||||
),
|
||||
child: const Text(
|
||||
"Nessun documento allegato alla bozza.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: localFiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final file = localFiles[index];
|
||||
// Calcoliamo la dimensione in MB
|
||||
final sizeMb = (file.size / (1024 * 1024)).toStringAsFixed(2);
|
||||
|
||||
// Scegliamo un'icona in base al tipo di file
|
||||
final isPdf = file.extension?.toLowerCase() == 'pdf';
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
isPdf ? Icons.picture_as_pdf : Icons.image,
|
||||
color: isPdf ? Colors.red : Colors.blue,
|
||||
size: 32,
|
||||
),
|
||||
title: Text(
|
||||
file.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text("$sizeMb MB"),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<ServicesCubit>()
|
||||
.removeLocalAttachment(index),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||
import 'package:flux/features/services/models/service_model.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/attachment_section.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/customer_section.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/general_info_section.dart';
|
||||
import 'package:flux/features/services/ui/service_form_screen/services_grid.dart';
|
||||
@@ -113,7 +114,8 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
||||
ServicesGrid(service: service),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// TODO: _AttachmentsSection(),
|
||||
AttachmentsSection(),
|
||||
const SizedBox(height: 32),
|
||||
_buildBottomActionButtons(context, isSaving: isSaving),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user