@@ -1,8 +1,11 @@
|
||||
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_cubit.dart';
|
||||
import 'package:flux/core/widgets/image_viewer_widget.dart';
|
||||
import 'package:flux/core/widgets/pdf_viewer_widget.dart';
|
||||
import 'package:flux/core/widgets/qr_upload_dialog.dart';
|
||||
import 'package:flux/features/services/blocs/service_files_bloc.dart';
|
||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||
import 'package:flux/features/services/models/service_file_model.dart';
|
||||
|
||||
@@ -25,13 +28,16 @@ class AttachmentsSection extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ServicesCubit, ServicesState>(
|
||||
builder: (context, state) {
|
||||
final files = state.currentService?.files ?? [];
|
||||
ServiceFilesBloc serviceFilesBloc = BlocProvider.of<ServiceFilesBloc>(
|
||||
context,
|
||||
);
|
||||
|
||||
return BlocBuilder<ServiceFilesBloc, ServiceFilesState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- HEADER SEZIONE ---
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -43,16 +49,38 @@ class AttachmentsSection extends StatelessWidget {
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.attach_file),
|
||||
label: const Text("Aggiungi File"),
|
||||
onPressed: () => _pickFiles(context),
|
||||
Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
icon: const Icon(Icons.attach_file),
|
||||
label: const Text("Aggiungi File"),
|
||||
onPressed: () => _pickFiles(context),
|
||||
),
|
||||
if (!context.read<SessionCubit>().state.isMobileDevice) ...[
|
||||
const SizedBox(width: 12),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _handleGenerateQr(context),
|
||||
icon: const Icon(Icons.qr_code),
|
||||
label: const Text("GENERA QR"),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.1),
|
||||
foregroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
if (files.isEmpty)
|
||||
// --- LISTA VUOTA ---
|
||||
if (state.allFiles.isEmpty) // Furbata: usiamo allFiles.isEmpty!
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
@@ -70,34 +98,49 @@ class AttachmentsSection extends StatelessWidget {
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
else
|
||||
// --- LISTA PIENA ---
|
||||
else ...[
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: files.length,
|
||||
itemCount: state.allFiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final file = files[index];
|
||||
// Calcoliamo la dimensione in MB
|
||||
final file = state.allFiles[index];
|
||||
final sizeMb = (file.fileSize / (1024 * 1024))
|
||||
.toStringAsFixed(2);
|
||||
|
||||
// Scegliamo un'icona in base al tipo di file
|
||||
final isPdf = file.extension.toLowerCase() == 'pdf';
|
||||
final isSelected = state.selectedFiles.contains(file);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _handleSingleClick(context, file),
|
||||
onTap: () => serviceFilesBloc.add(
|
||||
ToggleServiceFileSelectionEvent(file),
|
||||
),
|
||||
onDoubleTap: () => _handleDoubleClick(context, file),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
elevation: 0,
|
||||
// UX Fina: cambiamo colore del bordo se selezionato
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
side: BorderSide(color: Colors.grey.shade300),
|
||||
side: BorderSide(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.grey.shade300,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
// UX Fina: Sfondo leggermente colorato se selezionato
|
||||
color: isSelected
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.05)
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
isPdf ? Icons.picture_as_pdf : Icons.image,
|
||||
color: isPdf ? Colors.red : Colors.blue,
|
||||
isSelected
|
||||
? Icons.check_box
|
||||
: Icons.check_box_outline_blank,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 32,
|
||||
),
|
||||
title: Text(
|
||||
@@ -105,35 +148,156 @@ class AttachmentsSection extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text("$sizeMb MB"),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<ServicesCubit>()
|
||||
.removeAttachment(index),
|
||||
subtitle: Text(
|
||||
file.isLocal ? "$sizeMb MB • (Nuovo)" : "$sizeMb MB",
|
||||
),
|
||||
trailing: Icon(
|
||||
isPdf ? Icons.picture_as_pdf : Icons.image,
|
||||
color: isPdf ? Colors.red : Colors.blue,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// --- PANNELLO AZIONI CONTESTUALI (LA MAGIA) ---
|
||||
// Appare SOLO se c'è almeno un file selezionato
|
||||
if (state.selectedFiles.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Contatore
|
||||
Text(
|
||||
"${state.selectedFiles.length} file selezionati",
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// Bottone Elimina
|
||||
TextButton.icon(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: Colors.red,
|
||||
),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
label: const Text("Elimina"),
|
||||
onPressed: () {
|
||||
// Qui lancerai l'evento per eliminare i file selezionati!
|
||||
// Es: serviceFilesBloc.add(DeleteSelectedFilesEvent());
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Bottone Copia
|
||||
ElevatedButton.icon(
|
||||
icon: const Icon(Icons.copy),
|
||||
label: const Text("Copia in Cliente"),
|
||||
onPressed: () => saveAndCopyFilesToCustomer(
|
||||
context,
|
||||
state.selectedFiles,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleGenerateQr(BuildContext context) async {
|
||||
final cubit = context.read<ServicesCubit>();
|
||||
var currentService = cubit.state.currentService;
|
||||
|
||||
// 1. SE LA PRATICA E' NUOVA (Manca l'ID)
|
||||
if (currentService == null || currentService.id == null) {
|
||||
// Chiediamo conferma
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text("Salvataggio Necessario"),
|
||||
content: const Text(
|
||||
"Per generare il QR Code e caricare file dal telefono, la pratica deve essere prima salvata in BOZZA.\n\nVuoi salvare ora?",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(ctx, false),
|
||||
child: const Text("Annulla"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(ctx, true),
|
||||
child: const Text("Salva in Bozza"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirm != true) return; // Utente ha annullato
|
||||
|
||||
// Salviamo forzatamente in bozza
|
||||
await cubit.saveCurrentService(isBozza: true);
|
||||
|
||||
// Recuperiamo il servizio aggiornato con l'ID!
|
||||
currentService = cubit.state.currentService;
|
||||
|
||||
if (currentService?.id == null) {
|
||||
// Se c'è stato un errore nel salvataggio, usciamo
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. ORA ABBIAMO L'ID SICURO -> MOSTRIAMO IL QR!
|
||||
if (context.mounted) {
|
||||
// Creiamo un nome leggibile da passare nel link
|
||||
final nomePratica = "Pratica ${currentService?.customerDisplayName ?? ''}"
|
||||
.trim();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => QrUploadDialog(
|
||||
deepLinkUrl:
|
||||
'fluxapp://service/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}',
|
||||
title: 'Scatta per\n$nomePratica',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// --- LOGICA DI COPIA AL CLIENTE ---
|
||||
void _handleSingleClick(BuildContext context, ServiceFileModel file) {
|
||||
void saveAndCopyFilesToCustomer(
|
||||
BuildContext context,
|
||||
List<ServiceFileModel> files,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text("Copia nei documenti Cliente"),
|
||||
content: const Text(
|
||||
"Vuoi copiare questo file nell'anagrafica del cliente? \n\n"
|
||||
"Vuoi copiare i file selezionati nell'anagrafica del cliente? \n\n"
|
||||
"Attenzione: per procedere, la pratica attuale verrà prima salvata in stato BOZZA.",
|
||||
),
|
||||
actions: [
|
||||
@@ -145,7 +309,7 @@ class AttachmentsSection extends StatelessWidget {
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
// 1. Diciamo al Cubit di salvare in Bozza e fare la copia
|
||||
context.read<ServicesCubit>().saveAndCopyFileToCustomer(file);
|
||||
context.read<ServicesCubit>().saveAndCopyFileToCustomer(files);
|
||||
},
|
||||
child: const Text("Salva e Copia"),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flux/features/services/blocs/service_files_bloc.dart';
|
||||
|
||||
class ServiceMobileUploadScreen extends StatelessWidget {
|
||||
final String serviceId;
|
||||
final String serviceName;
|
||||
|
||||
const ServiceMobileUploadScreen({
|
||||
super.key,
|
||||
required this.serviceId,
|
||||
required this.serviceName,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<ServiceFilesBloc, ServiceFilesState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == ServiceFilesStatus.success) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text("File caricato! ✅")));
|
||||
}
|
||||
if (state.status == ServiceFilesStatus.failure) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text("Errore: ${state.error}")));
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: Text("Upload Pratica:\n$serviceName")),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 80,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _handleCamera(context),
|
||||
icon: const Icon(Icons.camera_alt_rounded, size: 28),
|
||||
label: const Text(
|
||||
"SCATTA FOTO",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 80,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _handleFilePicker(context),
|
||||
icon: const Icon(Icons.file_present_rounded, size: 28),
|
||||
label: const Text(
|
||||
"CARICA DA MEMORIA",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[200],
|
||||
foregroundColor: Colors.black87,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleCamera(BuildContext context) async {
|
||||
final picker = ImagePicker();
|
||||
final photo = await picker.pickImage(
|
||||
source: ImageSource.camera,
|
||||
imageQuality: 80,
|
||||
);
|
||||
if (photo != null && context.mounted) {
|
||||
context.read<ServiceFilesBloc>().add(
|
||||
UploadServiceFilesEvent(photo: File(photo.path)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleFilePicker(BuildContext context) async {
|
||||
final result = await FilePicker.pickFiles(withData: true);
|
||||
if (result != null && context.mounted) {
|
||||
context.read<ServiceFilesBloc>().add(
|
||||
UploadServiceFilesEvent(pickedFile: result.files.first),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user