refactor pesantissimo dei Customer Files

This commit is contained in:
2026-04-23 11:20:34 +02:00
parent ba54267b77
commit 41f6d0dd43
25 changed files with 518 additions and 157 deletions

View File

@@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/data/core_repository.dart'; import 'package:flux/core/data/core_repository.dart';
import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart';
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/ui/customer_detail_screen.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart';
import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/home/ui/home_screen.dart';
@@ -90,9 +91,13 @@ class AppRouter {
builder: (context, state) { builder: (context, state) {
// Recuperiamo l'oggetto customer passato tramite extra // Recuperiamo l'oggetto customer passato tramite extra
final customer = state.extra as CustomerModel; final customer = state.extra as CustomerModel;
return CustomerDetailScreen(customer: customer); return BlocProvider(
create: (context) => CustomerFilesBloc(customer.id!),
child: CustomerDetailScreen(customer: customer),
);
}, },
), ),
GoRoute( GoRoute(
path: '/products', path: '/products',
name: 'products', name: 'products',

View File

@@ -0,0 +1,9 @@
// Funzione che chiede le chiavi a Supabase
import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
Future<String> getSignedUrl(String storagePath) async {
return await GetIt.I<SupabaseClient>().storage
.from('documents')
.createSignedUrl(storagePath, 60); // Link che si autodistrugge in 60s
}

View File

@@ -1,6 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; // <--- AGGIUNGI QUESTO import 'package:flux/core/utils/functions.dart';
class ImageViewerWidget extends StatelessWidget { class ImageViewerWidget extends StatelessWidget {
final String? storagePath; // ATTENZIONE: Ora contiene lo storagePath! final String? storagePath; // ATTENZIONE: Ora contiene lo storagePath!
@@ -12,13 +12,6 @@ class ImageViewerWidget extends StatelessWidget {
'Errore: Devi fornire un Path valido o i bytes del file!', 'Errore: Devi fornire un Path valido o i bytes del file!',
); );
// Funzione che chiede le chiavi a Supabase
Future<String> _getSignedUrl() async {
return await Supabase.instance.client.storage
.from('documents')
.createSignedUrl(storagePath!, 60); // Link che si autodistrugge in 60s
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -37,7 +30,7 @@ class ImageViewerWidget extends StatelessWidget {
child: bytes != null child: bytes != null
? Image.memory(bytes!) ? Image.memory(bytes!)
: FutureBuilder<String>( : FutureBuilder<String>(
future: _getSignedUrl(), future: getSignedUrl(storagePath!),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator(); return const CircularProgressIndicator();

View File

@@ -1,5 +1,6 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flux/core/utils/functions.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:pdfx/pdfx.dart'; import 'package:pdfx/pdfx.dart';
import 'package:internet_file/internet_file.dart'; import 'package:internet_file/internet_file.dart';
@@ -39,11 +40,7 @@ class _PdfViewerWidgetState extends State<PdfViewerWidget> {
pdfData = widget.bytes!; pdfData = widget.bytes!;
} else if (widget.storagePath != null && widget.storagePath!.isNotEmpty) { } else if (widget.storagePath != null && widget.storagePath!.isNotEmpty) {
// SCENARIO 2: Pratica salvata, scarichiamo da Supabase (Remoto) // SCENARIO 2: Pratica salvata, scarichiamo da Supabase (Remoto)
final signedUrl = await GetIt.I final signedUrl = await getSignedUrl(widget.storagePath!);
.get<SupabaseClient>()
.storage
.from('documents')
.createSignedUrl(widget.storagePath!, 60);
pdfData = await InternetFile.get(signedUrl); pdfData = await InternetFile.get(signedUrl);
} else { } else {
throw Exception("Nessun documento trovato"); throw Exception("Nessun documento trovato");

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.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_model.dart'; import 'package:flux/features/customers/models/customer_model.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';

View File

@@ -0,0 +1,93 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:get_it/get_it.dart';
part 'customer_files_events.dart';
part 'customer_files_state.dart';
class CustomerFilesBloc extends Bloc<CustomerFilesEvent, CustomerFilesState> {
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
final String customerId;
CustomerFilesBloc(this.customerId)
: super(const CustomerFilesState(status: CustomerFilesStatus.initial)) {
on<LoadCustomerFilesEvent>(_loadCustomerFiles);
on<UploadCustomerFileEvent>(_uploadCustomerFile);
on<DeleteCustomerFileEvent>(_deleteCustomerFile);
on<ToggleCustomerFileSelectionEvent>(_toggleCustomerFileSelection);
}
void _loadCustomerFiles(
LoadCustomerFilesEvent event,
Emitter<CustomerFilesState> emit,
) async {
await emit.forEach<List<CustomerFileModel>>(
_repository.getCustomerFilesStream(customerId),
onData: (customerFiles) => CustomerFilesState(
status: CustomerFilesStatus.success,
customerFiles: customerFiles,
),
onError: (error, stackTrace) => CustomerFilesState(
status: CustomerFilesStatus.failure,
error: error.toString(),
),
);
}
Future<void> _uploadCustomerFile(
UploadCustomerFileEvent event,
Emitter<CustomerFilesState> emit,
) async {
emit(state.copyWith(status: CustomerFilesStatus.uploading));
if (event.pickedFile != null) {
try {
await _repository.uploadAndRegisterFile(
customerId: customerId,
pickedFile: event.pickedFile!,
);
emit(state.copyWith(status: CustomerFilesStatus.success));
} catch (e) {
emit(
state.copyWith(
status: CustomerFilesStatus.failure,
error: e.toString(),
),
);
}
}
}
Future<void> _deleteCustomerFile(
DeleteCustomerFileEvent event,
Emitter<CustomerFilesState> emit,
) async {
emit(state.copyWith(status: CustomerFilesStatus.loading));
try {
await _repository.deleteDocument(event.file);
emit(state.copyWith(status: CustomerFilesStatus.success));
} catch (e) {
emit(
state.copyWith(
status: CustomerFilesStatus.failure,
error: e.toString(),
),
);
}
}
void _toggleCustomerFileSelection(
ToggleCustomerFileSelectionEvent event,
Emitter<CustomerFilesState> emit,
) {
List<CustomerFileModel> selectedFiles = List.from(state.selectedFiles);
if (selectedFiles.contains(event.file)) {
selectedFiles.remove(event.file);
} else {
selectedFiles.add(event.file);
}
emit(state.copyWith(selectedFiles: selectedFiles));
}
}

View File

@@ -0,0 +1,26 @@
part of 'customer_files_bloc.dart';
abstract class CustomerFilesEvent extends Equatable {
const CustomerFilesEvent();
@override
List<Object> get props => [];
}
class LoadCustomerFilesEvent extends CustomerFilesEvent {}
class UploadCustomerFileEvent extends CustomerFilesEvent {
final PlatformFile? pickedFile;
final File? photo;
const UploadCustomerFileEvent({this.pickedFile, this.photo});
}
class DeleteCustomerFileEvent extends CustomerFilesEvent {
final CustomerFileModel file;
const DeleteCustomerFileEvent(this.file);
}
class ToggleCustomerFileSelectionEvent extends CustomerFilesEvent {
final CustomerFileModel file;
const ToggleCustomerFileSelectionEvent(this.file);
}

View File

@@ -0,0 +1,34 @@
part of 'customer_files_bloc.dart';
enum CustomerFilesStatus { initial, loading, uploading, success, failure }
class CustomerFilesState extends Equatable {
const CustomerFilesState({
required this.status,
this.error,
this.customerFiles = const [],
this.selectedFiles = const [],
});
final CustomerFilesStatus status;
final String? error;
final List<CustomerFileModel> customerFiles;
final List<CustomerFileModel> selectedFiles;
@override
List<Object?> get props => [status, error, customerFiles, selectedFiles];
CustomerFilesState copyWith({
CustomerFilesStatus? status,
String? error,
List<CustomerFileModel>? customerFiles,
List<CustomerFileModel>? selectedFiles,
}) {
return CustomerFilesState(
status: status ?? this.status,
error: error,
customerFiles: customerFiles ?? this.customerFiles,
selectedFiles: selectedFiles ?? this.selectedFiles,
);
}
}

View File

@@ -1,18 +1,27 @@
part of 'customer_cubit.dart'; part of 'customer_cubit.dart';
enum CustomerStatus { initial, loading, success, failure } enum CustomerStatus {
initial,
loading,
filesLoading,
filesUploading,
success,
failure,
}
class CustomerState extends Equatable { class CustomerState extends Equatable {
final CustomerStatus status; final CustomerStatus status;
final List<CustomerModel> customers; final List<CustomerModel> customers;
final CustomerModel? lastCreatedCustomer; final CustomerModel? lastCreatedCustomer;
final String? errorMessage; final String? errorMessage;
final List<CustomerFileModel> customerFiles;
const CustomerState({ const CustomerState({
this.status = CustomerStatus.initial, this.status = CustomerStatus.initial,
this.customers = const [], this.customers = const [],
this.lastCreatedCustomer, this.lastCreatedCustomer,
this.errorMessage, this.errorMessage,
this.customerFiles = const [],
}); });
CustomerState copyWith({ CustomerState copyWith({
@@ -20,12 +29,14 @@ class CustomerState extends Equatable {
List<CustomerModel>? customers, List<CustomerModel>? customers,
CustomerModel? lastCreatedCustomer, CustomerModel? lastCreatedCustomer,
String? errorMessage, String? errorMessage,
List<CustomerFileModel>? customerFiles,
}) { }) {
return CustomerState( return CustomerState(
status: status ?? this.status, status: status ?? this.status,
customers: customers ?? this.customers, customers: customers ?? this.customers,
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer, lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
customerFiles: customerFiles ?? this.customerFiles,
); );
} }
@@ -35,5 +46,6 @@ class CustomerState extends Equatable {
customers, customers,
lastCreatedCustomer, lastCreatedCustomer,
errorMessage, errorMessage,
customerFiles,
]; ];
} }

View File

@@ -1,5 +1,8 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/functions.dart';
import 'package:flux/core/utils/string_extensions.dart'; import 'package:flux/core/utils/string_extensions.dart';
import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@@ -76,6 +79,19 @@ class CustomerRepository {
} }
} }
/// Ascolta in tempo reale i file caricati per un cliente
Stream<List<CustomerFileModel>> getCustomerFilesStream(String customerId) {
return _supabase
.from('customer_file')
.stream(primaryKey: ['id'])
.eq('customer_id', customerId)
.order('created_at', ascending: false)
.map(
(listOfMaps) =>
listOfMaps.map((map) => CustomerFileModel.fromMap(map)).toList(),
);
}
/// Recupera i file di un cliente specifico /// Recupera i file di un cliente specifico
Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async { Future<List<CustomerFileModel>> getCustomerFiles(String customerId) async {
try { try {
@@ -113,7 +129,7 @@ class CustomerRepository {
customerId: customerId, customerId: customerId,
name: cleanFileName.fileNameWithoutExtension(), name: cleanFileName.fileNameWithoutExtension(),
extension: cleanFileName.fileExtension(), extension: cleanFileName.fileExtension(),
url: storagePath, storagePath: storagePath,
fileSize: fileSize, fileSize: fileSize,
); );
final String mimeType = fileToSave.extension.toLowerCase() == 'pdf' final String mimeType = fileToSave.extension.toLowerCase() == 'pdf'
@@ -161,9 +177,13 @@ class CustomerRepository {
} }
/// Elimina un file dallo storage /// Elimina un file dallo storage
Future<void> deleteDocument(String fullPath) async { Future<void> deleteDocument(CustomerFileModel file) async {
// Il path dovrebbe essere ricavato dall'URL try {
final path = fullPath.split('documents/').last; final path = await getSignedUrl(file.storagePath);
await _supabase.from('customer_file').delete().eq('id', file.id!);
await _supabase.storage.from('documents').remove([path]); await _supabase.storage.from('documents').remove([path]);
} on Exception catch (e) {
throw 'Errore durante l\'eliminazione del file: $e';
}
} }
} }

View File

@@ -4,7 +4,7 @@ class CustomerFileModel extends Equatable {
final String? id; final String? id;
final String customerId; // Riferimento UUID final String customerId; // Riferimento UUID
final String name; final String name;
final String url; final String storagePath;
final String extension; final String extension;
final DateTime? createdAt; final DateTime? createdAt;
final int fileSize; final int fileSize;
@@ -13,7 +13,7 @@ class CustomerFileModel extends Equatable {
this.id, this.id,
required this.customerId, required this.customerId,
required this.name, required this.name,
required this.url, required this.storagePath,
required this.extension, required this.extension,
this.createdAt, this.createdAt,
required this.fileSize, required this.fileSize,
@@ -35,7 +35,7 @@ class CustomerFileModel extends Equatable {
String? id, String? id,
String? customerId, String? customerId,
String? name, String? name,
String? url, String? storagePath,
String? extension, String? extension,
DateTime? createdAt, DateTime? createdAt,
int? fileSize, int? fileSize,
@@ -44,7 +44,7 @@ class CustomerFileModel extends Equatable {
id: id ?? this.id, id: id ?? this.id,
customerId: customerId ?? this.customerId, customerId: customerId ?? this.customerId,
name: name ?? this.name, name: name ?? this.name,
url: url ?? this.url, storagePath: storagePath ?? this.storagePath,
extension: extension ?? this.extension, extension: extension ?? this.extension,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
fileSize: fileSize ?? this.fileSize, fileSize: fileSize ?? this.fileSize,
@@ -56,7 +56,7 @@ class CustomerFileModel extends Equatable {
id: map['id'] as String, id: map['id'] as String,
customerId: map['customer_id'], customerId: map['customer_id'],
name: map['name'], name: map['name'],
url: map['url'], storagePath: map['storage_path'],
extension: map['extension'] ?? '', extension: map['extension'] ?? '',
createdAt: map['created_at'] != null createdAt: map['created_at'] != null
? DateTime.parse(map['created_at']) ? DateTime.parse(map['created_at'])
@@ -72,7 +72,7 @@ class CustomerFileModel extends Equatable {
if (id != null) 'id': id, if (id != null) 'id': id,
'customer_id': customerId, 'customer_id': customerId,
'name': name, 'name': name,
'url': url, 'storage_path': storagePath,
'extension': extension, 'extension': extension,
'file_size': fileSize, 'file_size': fileSize,
}; };
@@ -83,7 +83,7 @@ class CustomerFileModel extends Equatable {
id, id,
customerId, customerId,
name, name,
url, storagePath,
extension, extension,
createdAt, createdAt,
fileSize, fileSize,

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/core/widgets/image_viewer_widget.dart';
import 'package:flux/core/widgets/pdf_viewer_widget.dart';
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:flux/features/customers/models/customer_file_model.dart';
import 'package:get_it/get_it.dart';
class CustomerDetailScreen extends StatefulWidget { class CustomerDetailScreen extends StatefulWidget {
final CustomerModel customer; final CustomerModel customer;
@@ -15,36 +17,19 @@ class CustomerDetailScreen extends StatefulWidget {
} }
class _CustomerDetailScreenState extends State<CustomerDetailScreen> { class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
final _repository = GetIt.I<CustomerRepository>();
List<CustomerFileModel> _files = [];
bool _isLoadingFiles = true;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadFiles(); _loadFiles();
} }
Future<void> _loadFiles() async { void _loadFiles() {
try { context.read<CustomerFilesBloc>().add(LoadCustomerFilesEvent());
final files = await _repository.getCustomerFiles(
widget.customer.id.toString(),
);
setState(() {
_files = files;
_isLoadingFiles = false;
});
} catch (e) {
setState(() => _isLoadingFiles = false);
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(e.toString())));
}
}
} }
Future<void> _pickAndUpload() async { Future<void> _pickAndUpload() async {
CustomerFilesBloc customerFilesBloc = context.read<CustomerFilesBloc>();
// Chiamata statica pulita // Chiamata statica pulita
FilePickerResult? result = await FilePicker.pickFiles( FilePickerResult? result = await FilePicker.pickFiles(
allowMultiple: true, allowMultiple: true,
@@ -55,11 +40,9 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
if (result != null) { if (result != null) {
for (var pickedFile in result.files) { for (var pickedFile in result.files) {
try { try {
final newFile = await _repository.uploadAndRegisterFile( customerFilesBloc.add(
customerId: widget.customer.id.toString(), UploadCustomerFileEvent(pickedFile: pickedFile),
pickedFile: pickedFile,
); );
setState(() => _files.add(newFile));
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -158,6 +141,8 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
} }
Widget _buildDocumentSection() { Widget _buildDocumentSection() {
return BlocBuilder<CustomerFilesBloc, CustomerFilesState>(
builder: (context, state) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -180,9 +165,9 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
if (_isLoadingFiles) if (state.status == CustomerFilesStatus.loading)
const Center(child: CircularProgressIndicator()) const Center(child: CircularProgressIndicator())
else if (_files.isEmpty) else if (state.customerFiles.isEmpty)
const Center(child: Text("Nessun documento presente")) const Center(child: Text("Nessun documento presente"))
else else
Expanded( Expanded(
@@ -193,12 +178,15 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
crossAxisSpacing: 16, crossAxisSpacing: 16,
childAspectRatio: 1.2, childAspectRatio: 1.2,
), ),
itemCount: _files.length, itemCount: state.customerFiles.length,
itemBuilder: (context, index) => _FileCard(file: _files[index]), itemBuilder: (context, index) =>
_FileCard(file: state.customerFiles[index], state: state),
), ),
), ),
], ],
); );
},
);
} }
Widget _infoTile(IconData icon, String label, String value) { Widget _infoTile(IconData icon, String label, String value) {
@@ -227,11 +215,19 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
class _FileCard extends StatelessWidget { class _FileCard extends StatelessWidget {
final CustomerFileModel file; final CustomerFileModel file;
const _FileCard({required this.file}); final CustomerFilesState state;
const _FileCard({required this.file, required this.state});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return GestureDetector(
onTap: () => context.read<CustomerFilesBloc>().add(
ToggleCustomerFileSelectionEvent(file),
),
onDoubleTap: () => _handleDoubleClickOnFile(context, file),
child: Stack(
children: [
Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.background, color: context.background,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@@ -240,7 +236,11 @@ class _FileCard extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(_getFileIcon(file.extension), size: 48, color: context.accent), Icon(
_getFileIcon(file.extension),
size: 48,
color: context.accent,
),
const SizedBox(height: 8), const SizedBox(height: 8),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -248,9 +248,21 @@ class _FileCard extends StatelessWidget {
file.name, file.name,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
), ),
), ),
),
],
),
),
if (state.selectedFiles.contains(file))
Positioned(
top: 10,
left: 10,
child: Icon(Icons.check_circle, color: context.accent, size: 24),
),
], ],
), ),
); );
@@ -268,4 +280,25 @@ class _FileCard extends StatelessWidget {
return Icons.insert_drive_file; return Icons.insert_drive_file;
} }
} }
void _handleDoubleClickOnFile(BuildContext context, CustomerFileModel file) {
showDialog(
context: context,
barrierDismissible: true,
builder: (ctx) => Dialog(
insetPadding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
width: double.infinity,
height: MediaQuery.of(context).size.height * 0.8,
child: file.isPdf
? PdfViewerWidget(storagePath: file.storagePath)
: ImageViewerWidget(storagePath: file.storagePath),
),
),
),
);
}
} }

View File

@@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/customers/blocs/customer_cubit.dart'; import 'package:flux/features/customers/blocs/customer_cubit.dart';
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/ui/customer_form.dart'; import 'package:flux/features/customers/ui/customer_form.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -37,39 +38,6 @@ class _CustomersContentState extends State<CustomersContent> {
} }
} }
/// Funzione unica per gestire Creazione e Modifica
void _openCustomerForm({CustomerModel? customer}) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: context.background,
content: SizedBox(
width: 500, // Larghezza ottimale per desktop
child: CustomerForm(
customer: customer,
onSave: (customerFromForm) {
final session = context.read<SessionCubit>().state;
final companyId = session.company?.id;
if (companyId == null) return;
if (customer == null) {
// CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create
context.read<CustomerCubit>().createCustomer(
customerFromForm.copyWith(companyId: companyId),
);
} else {
// CASO MODIFICA: L'ID e il companyId sono già nel modello
context.read<CustomerCubit>().updateCustomer(customerFromForm);
}
Navigator.pop(dialogContext);
},
),
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -85,7 +53,7 @@ class _CustomersContentState extends State<CustomersContent> {
Padding( Padding(
padding: const EdgeInsets.only(right: 16), padding: const EdgeInsets.only(right: 16),
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => _openCustomerForm(), onPressed: () => openCustomerForm(context: context),
icon: const Icon(Icons.person_add_alt_1_rounded, size: 20), icon: const Icon(Icons.person_add_alt_1_rounded, size: 20),
label: const Text('NUOVO'), label: const Text('NUOVO'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@@ -244,8 +212,48 @@ class _CustomerTile extends StatelessWidget {
], ],
), ),
), ),
trailing: Icon(Icons.edit_note_rounded, color: context.accent), trailing: IconButton(
onPressed: () =>
openCustomerForm(context: context, customer: customer),
icon: Icon(Icons.edit_note_rounded, color: context.accent),
),
), ),
); );
} }
} }
/// Funzione unica per gestire Creazione e Modifica
void openCustomerForm({
CustomerModel? customer,
required BuildContext context,
}) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
backgroundColor: context.background,
content: SizedBox(
width: 500, // Larghezza ottimale per desktop
child: CustomerForm(
customer: customer,
onSave: (customerFromForm) {
final session = context.read<SessionCubit>().state;
final companyId = session.company?.id;
if (companyId == null) return;
if (customer == null) {
// CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create
context.read<CustomerCubit>().createCustomer(
customerFromForm.copyWith(companyId: companyId),
);
} else {
// CASO MODIFICA: L'ID e il companyId sono già nel modello
context.read<CustomerCubit>().updateCustomer(customerFromForm);
}
Navigator.pop(dialogContext);
},
),
),
),
);
}

View File

@@ -235,7 +235,7 @@ class ServicesCubit extends Cubit<ServicesState> {
serviceId: state.currentService?.id ?? '', serviceId: state.currentService?.id ?? '',
name: file.name.fileNameWithoutExtension(), name: file.name.fileNameWithoutExtension(),
extension: file.name.fileExtension(), extension: file.name.fileExtension(),
url: '', storagePath: '',
fileSize: file.size, fileSize: file.size,
localBytes: file.bytes, localBytes: file.bytes,
createdAt: DateTime.now(), createdAt: DateTime.now(),
@@ -295,7 +295,7 @@ class ServicesCubit extends Cubit<ServicesState> {
orElse: () => file, orElse: () => file,
); );
if (savedFile.url.isEmpty) { if (savedFile.storagePath.isEmpty) {
throw Exception( throw Exception(
"Errore: URL del file non trovato dopo il salvataggio.", "Errore: URL del file non trovato dopo il salvataggio.",
); );

View File

@@ -159,7 +159,10 @@ class ServicesRepository {
final String mimeType = file.extension.toLowerCase() == 'pdf' final String mimeType = file.extension.toLowerCase() == 'pdf'
? 'application/pdf' ? 'application/pdf'
: 'image/${file.extension}'; : 'image/${file.extension}';
final fileToSave = file.copyWith(serviceId: newId, url: storagePath); final fileToSave = file.copyWith(
serviceId: newId,
storagePath: storagePath,
);
// Creiamo una funzione asincrona per caricare file e scrivere nel DB // Creiamo una funzione asincrona per caricare file e scrivere nel DB
Future<void> uploadAndLink() async { Future<void> uploadAndLink() async {
@@ -235,6 +238,15 @@ class ServicesRepository {
} }
} }
/// Ascolta in tempo reale i file caricati per una pratica
Stream<List<Map<String, dynamic>>> getServiceFilesStream(String serviceId) {
return _supabase
.from('service_file')
.stream(primaryKey: ['id'])
.eq('service_id', serviceId)
.order('created_at', ascending: false);
}
Future<void> copyFileToCustomer({ Future<void> copyFileToCustomer({
required ServiceFileModel file, required ServiceFileModel file,
required String customerId, required String customerId,
@@ -242,7 +254,7 @@ class ServicesRepository {
CustomerFileModel fileToCopy = CustomerFileModel( CustomerFileModel fileToCopy = CustomerFileModel(
customerId: customerId, customerId: customerId,
name: file.name, name: file.name,
url: file.url, storagePath: file.storagePath,
extension: file.extension, extension: file.extension,
fileSize: file.fileSize, fileSize: file.fileSize,
); );

View File

@@ -7,7 +7,7 @@ class ServiceFileModel extends Equatable {
final DateTime? createdAt; final DateTime? createdAt;
final String name; final String name;
final String extension; final String extension;
final String url; final String storagePath;
final String serviceId; final String serviceId;
final int fileSize; final int fileSize;
final Uint8List? localBytes; final Uint8List? localBytes;
@@ -17,7 +17,7 @@ class ServiceFileModel extends Equatable {
this.createdAt, this.createdAt,
required this.name, required this.name,
required this.extension, required this.extension,
required this.url, required this.storagePath,
required this.serviceId, required this.serviceId,
required this.fileSize, required this.fileSize,
this.localBytes, this.localBytes,
@@ -40,7 +40,7 @@ class ServiceFileModel extends Equatable {
DateTime? createdAt, DateTime? createdAt,
String? name, String? name,
String? extension, String? extension,
String? url, String? storagePath,
String? serviceId, String? serviceId,
int? fileSize, int? fileSize,
Uint8List? localBytes, Uint8List? localBytes,
@@ -50,7 +50,7 @@ class ServiceFileModel extends Equatable {
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
name: name ?? this.name, name: name ?? this.name,
extension: extension ?? this.extension, extension: extension ?? this.extension,
url: url ?? this.url, storagePath: storagePath ?? this.storagePath,
serviceId: serviceId ?? this.serviceId, serviceId: serviceId ?? this.serviceId,
fileSize: fileSize ?? this.fileSize, fileSize: fileSize ?? this.fileSize,
localBytes: localBytes ?? this.localBytes, localBytes: localBytes ?? this.localBytes,
@@ -65,7 +65,7 @@ class ServiceFileModel extends Equatable {
: null, : null,
name: map['name'] ?? '', name: map['name'] ?? '',
extension: map['extension'] ?? '', extension: map['extension'] ?? '',
url: map['url'] ?? '', storagePath: map['storage_path'] ?? '',
serviceId: map['service_id']?.toString() ?? '', serviceId: map['service_id']?.toString() ?? '',
fileSize: map['file_size'] is int fileSize: map['file_size'] is int
? map['file_size'] ? map['file_size']
@@ -78,7 +78,7 @@ class ServiceFileModel extends Equatable {
if (id != null) 'id': id, if (id != null) 'id': id,
'name': name, 'name': name,
'extension': extension, 'extension': extension,
'url': url, 'storage_path': storagePath,
'service_id': serviceId, 'service_id': serviceId,
'file_size': fileSize, 'file_size': fileSize,
}; };
@@ -90,7 +90,7 @@ class ServiceFileModel extends Equatable {
createdAt, createdAt,
name, name,
extension, extension,
url, storagePath,
serviceId, serviceId,
fileSize, fileSize,
localBytes, localBytes,

View File

@@ -169,11 +169,15 @@ class AttachmentsSection extends StatelessWidget {
height: MediaQuery.of(context).size.height * 0.8, height: MediaQuery.of(context).size.height * 0.8,
child: file.isPdf child: file.isPdf
? PdfViewerWidget( ? PdfViewerWidget(
storagePath: file.url.isNotEmpty ? file.url : null, storagePath: file.storagePath.isNotEmpty
? file.storagePath
: null,
bytes: file.localBytes, bytes: file.localBytes,
) )
: ImageViewerWidget( : ImageViewerWidget(
storagePath: file.url.isNotEmpty ? file.url : null, storagePath: file.storagePath.isNotEmpty
? file.storagePath
: null,
bytes: file.localBytes, bytes: file.localBytes,
), ),
), ),

View File

@@ -6,10 +6,14 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
#include <gtk/gtk_plugin.h> #include <gtk/gtk_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) gtk_registrar = g_autoptr(FlPluginRegistrar) gtk_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
gtk_plugin_register_with_registrar(gtk_registrar); gtk_plugin_register_with_registrar(gtk_registrar);

View File

@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
gtk gtk
url_launcher_linux url_launcher_linux
) )

View File

@@ -7,6 +7,7 @@ import Foundation
import app_links import app_links
import file_picker import file_picker
import file_selector_macos
import pdfx import pdfx
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos import url_launcher_macos
@@ -14,6 +15,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin")) PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@@ -3,6 +3,8 @@ PODS:
- FlutterMacOS - FlutterMacOS
- file_picker (0.0.1): - file_picker (0.0.1):
- FlutterMacOS - FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- pdfx (1.0.0): - pdfx (1.0.0):
- FlutterMacOS - FlutterMacOS
@@ -15,6 +17,7 @@ PODS:
DEPENDENCIES: DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`) - file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`) - pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -25,6 +28,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
file_picker: file_picker:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos :path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
file_selector_macos:
:path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos
FlutterMacOS: FlutterMacOS:
:path: Flutter/ephemeral :path: Flutter/ephemeral
pdfx: pdfx:
@@ -37,6 +42,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS: SPEC CHECKSUMS:
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51 pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb

View File

@@ -185,6 +185,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "11.0.2" version: "11.0.2"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0"
url: "https://pub.dev"
source: hosted
version: "0.9.4"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
url: "https://pub.dev"
source: hosted
version: "0.9.5"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85"
url: "https://pub.dev"
source: hosted
version: "2.7.0"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd"
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -328,6 +360,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622"
url: "https://pub.dev"
source: hosted
version: "0.8.13+16"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
url: "https://pub.dev"
source: hosted
version: "0.8.13+6"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
url: "https://pub.dev"
source: hosted
version: "0.2.2+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c"
url: "https://pub.dev"
source: hosted
version: "2.11.1"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
source: hosted
version: "0.2.2"
internet_file: internet_file:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -18,6 +18,7 @@ dependencies:
get_it: ^9.2.1 get_it: ^9.2.1
go_router: ^17.2.0 go_router: ^17.2.0
google_fonts: ^8.0.2 google_fonts: ^8.0.2
image_picker: ^1.2.1
internet_file: ^1.3.0 internet_file: ^1.3.0
intl: ^0.20.2 intl: ^0.20.2
pdfx: ^2.9.2 pdfx: ^2.9.2

View File

@@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h> #include <app_links/app_links_plugin_c_api.h>
#include <file_selector_windows/file_selector_windows.h>
#include <pdfx/pdfx_plugin.h> #include <pdfx/pdfx_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h> #include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
@@ -14,6 +15,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar( AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi")); registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
PdfxPluginRegisterWithRegistrar( PdfxPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PdfxPlugin")); registry->GetRegistrarForPlugin("PdfxPlugin"));
PermissionHandlerWindowsPluginRegisterWithRegistrar( PermissionHandlerWindowsPluginRegisterWithRegistrar(

View File

@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
app_links app_links
file_selector_windows
pdfx pdfx
permission_handler_windows permission_handler_windows
url_launcher_windows url_launcher_windows