fix mobile upload

This commit is contained in:
2026-05-09 09:50:20 +02:00
parent c6ef798b22
commit 65aa3c7de8
11 changed files with 164 additions and 52 deletions

View File

@@ -4,7 +4,6 @@ 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/core/layout/app_shell.dart'; import 'package:flux/core/layout/app_shell.dart';
import 'package:flux/core/utils/extensions.dart';
import 'package:flux/core/widgets/set_password_screen.dart'; import 'package:flux/core/widgets/set_password_screen.dart';
import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart'; import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart';
import 'package:flux/core/widgets/shared_forms/upload_success_screen.dart'; import 'package:flux/core/widgets/shared_forms/upload_success_screen.dart';
@@ -285,6 +284,7 @@ class AppRouter {
builder: (context, state) { builder: (context, state) {
final typeString = state.pathParameters['type']!; final typeString = state.pathParameters['type']!;
final id = state.pathParameters['id']!; final id = state.pathParameters['id']!;
final companyId = state.uri.queryParameters['companyId']!;
// Trasformiamo la stringa dell'URL nel nostro amato Enum! // Trasformiamo la stringa dell'URL nel nostro amato Enum!
final parentType = AttachmentParentType.values.firstWhere( final parentType = AttachmentParentType.values.firstWhere(
@@ -297,8 +297,9 @@ class AppRouter {
return BlocProvider( return BlocProvider(
create: (context) => create: (context) =>
AttachmentsBloc(parentId: id, parentType: parentType), AttachmentsBloc(parentId: id, parentType: parentType),
child: const SharedMobileUploadScreen( child: SharedMobileUploadScreen(
title: 'Caricamento Rapido', title: 'Caricamento Rapido',
companyId: companyId,
), ),
); );
}, },

View File

@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
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/widgets/qr_upload_dialog.dart';
import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart';
import 'package:flux/features/attachments/ui/attachment_viewer_screen.dart'; import 'package:flux/features/attachments/ui/attachment_viewer_screen.dart';
import 'package:flux/features/attachments/ui/quick_rename_dialog.dart'; import 'package:flux/features/attachments/ui/quick_rename_dialog.dart';
@@ -30,14 +32,16 @@ class _ExportItem {
class SharedAttachmentsSection extends StatefulWidget { class SharedAttachmentsSection extends StatefulWidget {
final String? parentId; final String? parentId;
final String customerDisplayName; final String titleForUpload;
final AttachmentParentType parentType; final AttachmentParentType parentType;
final Future<String?> Function()? onGenerateIdForQr;
const SharedAttachmentsSection({ const SharedAttachmentsSection({
super.key, super.key,
this.parentId, this.parentId,
this.customerDisplayName = 'Cliente_sconosciuto', this.titleForUpload = 'Cliente_sconosciuto',
required this.parentType, required this.parentType,
this.onGenerateIdForQr,
}); });
@override @override
@@ -65,7 +69,7 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
Future<void> _selectExportDirectory() async { Future<void> _selectExportDirectory() async {
final String? selectedDirectory = await FilePicker.getDirectoryPath( final String? selectedDirectory = await FilePicker.getDirectoryPath(
dialogTitle: 'Seleziona la cartella di esportazione per TIM/Citrix', dialogTitle: 'Seleziona la cartella di esportazione',
); );
if (selectedDirectory != null) { if (selectedDirectory != null) {
@@ -189,7 +193,7 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
} else { } else {
// Se sono più file uniti // Se sono più file uniti
suggestedName = '${widget.customerDisplayName}_Unito'; suggestedName = '${widget.titleForUpload}_Unito';
} }
if (!mounted) return; if (!mounted) return;
@@ -397,7 +401,6 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
// USIAMO IL TUO BLOC!
return BlocBuilder<AttachmentsBloc, AttachmentsState>( return BlocBuilder<AttachmentsBloc, AttachmentsState>(
builder: (context, state) { builder: (context, state) {
final allFiles = state.allFiles; final allFiles = state.allFiles;
@@ -421,7 +424,7 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
title: const Text( title: const Text(
'Cartella Export (Es. Citrix TIM)', 'Cartella Export (Es. TIM AttachmentRepository)',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
subtitle: Text( subtitle: Text(
@@ -451,6 +454,69 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
onPressed: state.status == AttachmentsStatus.uploading onPressed: state.status == AttachmentsStatus.uploading
? null ? null
: _pickFiles, : _pickFiles,
/* : () {
final bloc = context.read<AttachmentsBloc>();
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => BlocProvider.value(
value: bloc,
child: SharedMobileUploadScreen(
title: widget.titleForUpload,
),
),
),
);
}, */
),
const SizedBox(width: 8),
Tooltip(
message: 'Carica foto con lo smartphone',
child: IconButton(
icon: const Icon(Icons.qr_code_scanner),
color: theme.colorScheme.primary, // Sempre colorato!
onPressed: () async {
String? targetId = state.parentId;
// SE L'ID NON C'È, CHIAMIAMO IL SALVATAGGIO IN BACKGROUND!
if (targetId == null) {
if (widget.onGenerateIdForQr != null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Salvataggio rapido scheda in corso... ⏳',
),
duration: Duration(seconds: 1),
),
);
// Aspettiamo che il TicketFormCubit faccia il suo lavoro
targetId = await widget.onGenerateIdForQr!();
}
// Se fallisce (es. validazione form non passata), ci fermiamo
if (targetId == null) return;
}
// GENERAZIONE DEL DEEP LINK AGNOSTICO
final companyId = GetIt.I
.get<SessionCubit>()
.state
.company!
.id!;
final deepLink =
'https://flux.catelli.it/upload/${state.parentType.name}/$targetId?companyId=$companyId';
if (context.mounted) {
showDialog(
context: context,
builder: (_) => QrUploadDialog(
deepLinkUrl: deepLink,
title: 'Carica File: ${widget.titleForUpload}',
),
);
}
},
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),

View File

@@ -8,8 +8,13 @@ import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
class SharedMobileUploadScreen extends StatefulWidget { class SharedMobileUploadScreen extends StatefulWidget {
final String title; final String title;
final String companyId;
const SharedMobileUploadScreen({super.key, required this.title}); const SharedMobileUploadScreen({
super.key,
required this.title,
required this.companyId,
});
@override @override
State<SharedMobileUploadScreen> createState() => State<SharedMobileUploadScreen> createState() =>
@@ -290,7 +295,10 @@ class _SharedMobileUploadScreenState extends State<SharedMobileUploadScreen> {
} }
Future<void> _handleFilePicker() async { Future<void> _handleFilePicker() async {
final result = await FilePicker.pickFiles(allowMultiple: true); final result = await FilePicker.pickFiles(
allowMultiple: true,
withData: true,
);
if (result != null) { if (result != null) {
setState(() { setState(() {
_stagedFiles.addAll(result.files); _stagedFiles.addAll(result.files);
@@ -304,7 +312,10 @@ class _SharedMobileUploadScreenState extends State<SharedMobileUploadScreen> {
// Lanciamo l'evento del nostro nuovo AttachmentsBloc Agnostico! // Lanciamo l'evento del nostro nuovo AttachmentsBloc Agnostico!
context.read<AttachmentsBloc>().add( context.read<AttachmentsBloc>().add(
UploadAttachmentsEvent(pickedFiles: _stagedFiles), UploadAttachmentsEvent(
pickedFiles: _stagedFiles,
companyId: widget.companyId,
),
); );
} }
} }

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/widgets/qr_upload_dialog.dart'; import 'package:flux/core/widgets/qr_upload_dialog.dart';
import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart'; import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart';
import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
import 'package:get_it/get_it.dart';
class SharedFilesSection extends StatelessWidget { class SharedFilesSection extends StatelessWidget {
final String titleNameForUpload; final String titleNameForUpload;
@@ -64,8 +66,13 @@ class SharedFilesSection extends StatelessWidget {
} }
// GENERAZIONE DEL DEEP LINK AGNOSTICO // GENERAZIONE DEL DEEP LINK AGNOSTICO
final companyId = GetIt.I
.get<SessionCubit>()
.state
.company!
.id!;
final deepLink = final deepLink =
'https://flux.catelli.it/upload/${state.parentType.name}/$targetId'; 'https://flux.catelli.it/upload/${state.parentType.name}/$targetId?companyId=$companyId';
if (context.mounted) { if (context.mounted) {
showDialog( showDialog(
@@ -93,6 +100,11 @@ class SharedFilesSection extends StatelessWidget {
value: bloc, value: bloc,
child: SharedMobileUploadScreen( child: SharedMobileUploadScreen(
title: titleNameForUpload, title: titleNameForUpload,
companyId: GetIt.I
.get<SessionCubit>()
.state
.company!
.id!,
), ),
), ),
), ),

View File

@@ -14,6 +14,7 @@ part 'attachments_state.dart';
class AttachmentsBloc extends Bloc<AttachmentsEvent, AttachmentsState> { class AttachmentsBloc extends Bloc<AttachmentsEvent, AttachmentsState> {
final _repository = GetIt.I.get<AttachmentsRepository>(); final _repository = GetIt.I.get<AttachmentsRepository>();
final String? companyId = GetIt.I.get<SessionCubit>().state.company?.id;
AttachmentsBloc({String? parentId, required AttachmentParentType parentType}) AttachmentsBloc({String? parentId, required AttachmentParentType parentType})
: super( : super(
@@ -36,7 +37,7 @@ class AttachmentsBloc extends Bloc<AttachmentsEvent, AttachmentsState> {
on<ClearAttachmentSelectionEvent>(_onClearAttachmentSelection); on<ClearAttachmentSelectionEvent>(_onClearAttachmentSelection);
// Se il BLoC nasce già con un ID, carichiamo i file // Se il BLoC nasce già con un ID, carichiamo i file
if (parentId != null) { if (parentId != null && companyId != null) {
add(LoadAttachmentsEvent(parentId: parentId)); add(LoadAttachmentsEvent(parentId: parentId));
} }
} }
@@ -65,6 +66,7 @@ class AttachmentsBloc extends Bloc<AttachmentsEvent, AttachmentsState> {
parentId: event.newParentId, parentId: event.newParentId,
parentType: state.parentType, parentType: state.parentType,
pickedFile: fakePlatformFile, pickedFile: fakePlatformFile,
companyId: companyId!,
); );
}).toList(); }).toList();
@@ -118,12 +120,11 @@ class AttachmentsBloc extends Bloc<AttachmentsEvent, AttachmentsState> {
// BIVIO 1: PRATICA NUOVA (Salvataggio locale) // BIVIO 1: PRATICA NUOVA (Salvataggio locale)
if (currentId == null) { if (currentId == null) {
final companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
final newLocalFiles = event.files.map((file) { final newLocalFiles = event.files.map((file) {
// Assegniamo i campi dinamicamente in base al parentType! // Assegniamo i campi dinamicamente in base al parentType!
return AttachmentModel( return AttachmentModel(
id: null, id: null,
companyId: companyId, companyId: companyId!,
operationId: state.parentType == AttachmentParentType.operation operationId: state.parentType == AttachmentParentType.operation
? '' ? ''
: null, : null,
@@ -156,6 +157,7 @@ class AttachmentsBloc extends Bloc<AttachmentsEvent, AttachmentsState> {
parentId: currentId, parentId: currentId,
parentType: state.parentType, parentType: state.parentType,
pickedFile: file, pickedFile: file,
companyId: companyId!,
); );
}).toList(); }).toList();
@@ -191,6 +193,7 @@ class AttachmentsBloc extends Bloc<AttachmentsEvent, AttachmentsState> {
parentId: state.parentId!, parentId: state.parentId!,
parentType: state.parentType, parentType: state.parentType,
pickedFile: file, pickedFile: file,
companyId: event.companyId,
), ),
); );
} }
@@ -216,6 +219,7 @@ class AttachmentsBloc extends Bloc<AttachmentsEvent, AttachmentsState> {
parentId: state.parentId!, parentId: state.parentId!,
parentType: state.parentType, parentType: state.parentType,
pickedFile: fakePlatformFile, pickedFile: fakePlatformFile,
companyId: companyId!,
), ),
); );
} }

View File

@@ -29,7 +29,12 @@ class AddAttachmentsEvent extends AttachmentsEvent {
class UploadAttachmentsEvent extends AttachmentsEvent { class UploadAttachmentsEvent extends AttachmentsEvent {
final List<PlatformFile>? pickedFiles; final List<PlatformFile>? pickedFiles;
final List<XFile>? photos; final List<XFile>? photos;
const UploadAttachmentsEvent({this.pickedFiles, this.photos}); final String companyId;
const UploadAttachmentsEvent({
this.pickedFiles,
this.photos,
required this.companyId,
});
} }
class DeleteAttachmentsEvent extends AttachmentsEvent {} class DeleteAttachmentsEvent extends AttachmentsEvent {}

View File

@@ -16,6 +16,7 @@ class AttachmentsState extends Equatable {
final AttachmentParentType parentType; final AttachmentParentType parentType;
final AttachmentsStatus status; final AttachmentsStatus status;
final String? error; final String? error;
final List<AttachmentModel> localFiles; final List<AttachmentModel> localFiles;
final List<AttachmentModel> remoteFiles; final List<AttachmentModel> remoteFiles;
final List<AttachmentModel> selectedFiles; final List<AttachmentModel> selectedFiles;

View File

@@ -3,8 +3,6 @@ import 'package:file_picker/file_picker.dart';
import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/attachments/models/attachment_model.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
class AttachmentsRepository { class AttachmentsRepository {
final _supabase = Supabase.instance.client; final _supabase = Supabase.instance.client;
@@ -58,9 +56,9 @@ class AttachmentsRepository {
required String parentId, required String parentId,
required AttachmentParentType parentType, required AttachmentParentType parentType,
required PlatformFile pickedFile, required PlatformFile pickedFile,
required String companyId,
}) async { }) async {
try { try {
final companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
final extension = pickedFile.extension ?? pickedFile.name.split('.').last; final extension = pickedFile.extension ?? pickedFile.name.split('.').last;
final cleanName = pickedFile.name final cleanName = pickedFile.name
.replaceAll(RegExp(r'[^\w\s\.-]'), '') .replaceAll(RegExp(r'[^\w\s\.-]'), '')

View File

@@ -9,6 +9,7 @@ import 'package:flux/core/widgets/qr_upload_dialog.dart';
import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/attachments/models/attachment_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';
class CustomerDetailScreen extends StatefulWidget { class CustomerDetailScreen extends StatefulWidget {
final CustomerModel customer; final CustomerModel customer;
@@ -43,7 +44,12 @@ class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
if (result != null) { if (result != null) {
try { try {
attachmentsBloc.add(UploadAttachmentsEvent(pickedFiles: result.files)); attachmentsBloc.add(
UploadAttachmentsEvent(
pickedFiles: result.files,
companyId: GetIt.I.get<SessionCubit>().state.company!.id!,
),
);
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of( ScaffoldMessenger.of(

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/widgets/shared_forms/shared_files_section.dart';
import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
import 'package:flux/features/operations/blocs/operation_form_cubit.dart'; import 'package:flux/features/operations/blocs/operation_form_cubit.dart';
import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
@@ -246,10 +245,12 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
flex: 3, flex: 3,
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: SharedFilesSection( child: SharedAttachmentsSection(
titleNameForUpload: parentType: AttachmentParentType.operation,
parentId: state.operation.id,
titleForUpload:
state.operation.customerDisplayName ?? state.operation.customerDisplayName ??
'Nuova operazione', 'Nuova pratica',
onGenerateIdForQr: _generateIdForQr, onGenerateIdForQr: _generateIdForQr,
), ),
), ),
@@ -408,9 +409,9 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
color: _getStatusColor(displayStatus), color: _getStatusColor(displayStatus),
), ),
items: OperationStatus.values items: OperationStatus.values
.where( /* .where(
(s) => s != OperationStatus.draft, (s) => s != OperationStatus.draft,
) // Nascondiamo 'Bozza' dal menu ) // Nascondiamo 'Bozza' dal menu */
.map( .map(
(status) => DropdownMenuItem( (status) => DropdownMenuItem(
value: status, value: status,
@@ -456,7 +457,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
), ),
const Divider(height: 32), const Divider(height: 32),
_buildSectionTitle('Cliente & Riferimento'), //_buildSectionTitle('Cliente & Riferimento'),
SharedCustomerSection( SharedCustomerSection(
customerId: currentOp.customerId, customerId: currentOp.customerId,
customerName: currentOp.customerDisplayName, customerName: currentOp.customerDisplayName,
@@ -539,15 +540,18 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
const Divider(height: 32), const Divider(height: 32),
if (showFiles) ...[ if (showFiles) ...[
/* SharedAttachmentsSection( SharedAttachmentsSection(
parentType: AttachmentParentType.operation, parentType: AttachmentParentType.operation,
parentId: currentOp.id, parentId: currentOp.id,
), */ titleForUpload:
SharedFilesSection(
titleNameForUpload:
state.operation.customerDisplayName ?? 'Nuova pratica', state.operation.customerDisplayName ?? 'Nuova pratica',
onGenerateIdForQr: _generateIdForQr, onGenerateIdForQr: _generateIdForQr,
), ),
/* SharedFilesSection(
titleNameForUpload:
state.operation.customerDisplayName ?? 'Nuova pratica',
onGenerateIdForQr: _generateIdForQr,
), */
], ],
], ],
); );

View File

@@ -45,6 +45,7 @@ class DetailsSection extends StatelessWidget {
} }
void _showProviderModal(BuildContext context, String operationType) { void _showProviderModal(BuildContext context, String operationType) {
final OperationFormCubit cubit = context.read<OperationFormCubit>();
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@@ -103,28 +104,31 @@ class DetailsSection extends StatelessWidget {
); );
} }
return ListView.builder( return BlocProvider.value(
controller: scrollController, value: cubit,
itemCount: filteredProviders.length, child: ListView.builder(
itemBuilder: (context, index) { controller: scrollController,
final provider = filteredProviders[index]; itemCount: filteredProviders.length,
return ListTile( itemBuilder: (context, index) {
leading: const Icon(Icons.business), final provider = filteredProviders[index];
title: Text( return ListTile(
provider.name, leading: const Icon(Icons.business),
style: const TextStyle( title: Text(
fontWeight: FontWeight.bold, provider.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
), ),
), onTap: () {
onTap: () { context.read<OperationFormCubit>().updateFields(
context.read<OperationFormCubit>().updateFields( providerId: provider.id,
providerId: provider.id, providerDisplayName: provider.name,
providerDisplayName: provider.name, );
); Navigator.pop(modalContext);
Navigator.pop(modalContext); },
}, );
); },
}, ),
); );
}, },
), ),