Files
flux/lib/features/customers/ui/customer_detail_screen.dart

356 lines
11 KiB
Dart
Raw Normal View History

2026-04-11 12:40:03 +02:00
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
2026-04-11 12:40:03 +02:00
import 'package:flux/core/theme/theme.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/attachments/blocs/attachments_bloc.dart';
import 'package:flux/features/attachments/models/attachment_model.dart';
2026-04-11 12:40:03 +02:00
import 'package:flux/features/customers/models/customer_model.dart';
class CustomerDetailScreen extends StatefulWidget {
final CustomerModel customer;
const CustomerDetailScreen({super.key, required this.customer});
@override
State<CustomerDetailScreen> createState() => _CustomerDetailScreenState();
}
class _CustomerDetailScreenState extends State<CustomerDetailScreen> {
@override
void initState() {
super.initState();
_loadFiles();
}
void _loadFiles() {
context.read<AttachmentsBloc>().add(
LoadAttachmentsEvent(parentId: widget.customer.id),
);
2026-04-11 12:40:03 +02:00
}
Future<void> _pickAndUpload() async {
AttachmentsBloc attachmentsBloc = context.read<AttachmentsBloc>();
2026-04-11 12:40:03 +02:00
// Chiamata statica pulita
FilePickerResult? result = await FilePicker.pickFiles(
allowMultiple: true,
type: FileType.any,
withData: true, // Fondamentale per avere i bytes pronti se servono
);
if (result != null) {
try {
attachmentsBloc.add(UploadAttachmentsEvent(pickedFiles: result.files));
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text("$e")));
2026-04-11 12:40:03 +02:00
}
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: context.background,
appBar: AppBar(
title: Text(
widget.customer.name,
2026-04-11 12:40:03 +02:00
style: const TextStyle(fontWeight: FontWeight.bold),
),
backgroundColor: context.background,
elevation: 0,
),
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// COLONNA SINISTRA: ANAGRAFICA
Expanded(
flex: 1,
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: _buildInfoSection(),
),
),
// DIVISORE VERTICALE
VerticalDivider(
width: 1,
color: context.accent.withValues(alpha: 0.1),
),
// COLONNA DESTRA: DOCUMENTI
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.all(24),
child: _buildDocumentSection(),
),
),
],
),
);
}
Widget _buildInfoSection() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_infoTile(Icons.phone_android, "Telefono", widget.customer.phoneNumber),
2026-04-11 12:40:03 +02:00
_infoTile(
Icons.email_outlined,
"Email",
widget.customer.email.isEmpty ? "Non fornita" : widget.customer.email,
),
_infoTile(
Icons.notes_outlined,
"Note",
widget.customer.note.isEmpty
? "Nessun appunto"
: widget.customer.note,
),
const SizedBox(height: 20),
if (widget.customer.doNotDisturb)
2026-04-11 12:40:03 +02:00
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Row(
children: [
Icon(Icons.privacy_tip, color: Colors.red, size: 20),
SizedBox(width: 10),
Text(
"PRIVACY: Non disturbare",
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
);
}
Widget _buildDocumentSection() {
return BlocBuilder<AttachmentsBloc, AttachmentsState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
2026-04-11 12:40:03 +02:00
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"DOCUMENTI",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: context.accent,
),
),
// ZONA BOTTONI: Li mettiamo in una Row
Row(
children: [
// Bottone classico: c'è sempre (carica da disco locale)
ElevatedButton.icon(
onPressed: _pickAndUpload,
icon: const Icon(Icons.add_circle_outline),
label: const Text("CARICA FILE"),
),
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: state.selectedFiles.isEmpty
? null
: () => _showDeleteConfirmationDialog(
context: context,
files: state.selectedFiles,
),
icon: const Icon(Icons.delete_outline),
label: const Text("ELIMINA FILE"),
),
// Controlliamo se siamo su Desktop/Web per mostrare il QR
if (!context.read<SessionCubit>().state.isMobileDevice) ...[
const SizedBox(
width: 12,
), // Un po' di respiro tra i bottoni
ElevatedButton.icon(
onPressed: () {
showDialog(
context: context,
builder: (context) => QrUploadDialog(
deepLinkUrl:
'fluxapp://customer/${widget.customer.id}/upload?name=${Uri.encodeComponent(widget.customer.name)}',
title: 'Scatta per ${widget.customer.name}',
),
);
},
icon: const Icon(Icons.qr_code),
label: const Text("GENERA QR"),
style: ElevatedButton.styleFrom(
// Lo facciamo di un colore leggermente diverso per distinguerlo
backgroundColor: context.accent.withValues(
alpha: 0.1,
),
foregroundColor: context.accent,
elevation: 0,
),
),
],
],
),
],
2026-04-11 12:40:03 +02:00
),
const SizedBox(height: 20),
if (state.status == AttachmentsStatus.loading)
const Center(child: CircularProgressIndicator())
else if (state.allFiles.isEmpty)
const Center(child: Text("Nessun documento presente"))
else
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisSpacing: 16,
crossAxisSpacing: 16,
childAspectRatio: 1.2,
),
itemCount: state.allFiles.length,
itemBuilder: (context, index) =>
_FileCard(file: state.allFiles[index], state: state),
),
2026-04-11 12:40:03 +02:00
),
],
);
},
2026-04-11 12:40:03 +02:00
);
}
Widget _infoTile(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 18, color: context.accent),
const SizedBox(width: 8),
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(color: context.secondaryText, fontSize: 16),
),
],
),
);
}
void _showDeleteConfirmationDialog({
required BuildContext context,
required List<AttachmentModel> files,
}) {}
2026-04-11 12:40:03 +02:00
}
class _FileCard extends StatelessWidget {
final AttachmentModel file;
final AttachmentsState state;
const _FileCard({required this.file, required this.state});
2026-04-11 12:40:03 +02:00
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => context.read<AttachmentsBloc>().add(
ToggleAttachmentSelectionEvent(file),
2026-04-11 12:40:03 +02:00
),
onDoubleTap: () => _handleDoubleClickOnFile(context, file),
child: Stack(
2026-04-11 12:40:03 +02:00
children: [
Container(
decoration: BoxDecoration(
color: context.background,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: context.accent.withValues(alpha: 0.1)),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_getFileIcon(file.extension),
size: 48,
color: context.accent,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
file.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
2026-04-11 12:40:03 +02:00
),
),
if (state.selectedFiles.contains(file))
Positioned(
top: 10,
left: 10,
child: Icon(Icons.check_circle, color: context.accent, size: 24),
),
2026-04-11 12:40:03 +02:00
],
),
);
}
IconData _getFileIcon(String ext) {
switch (ext.toLowerCase()) {
case 'pdf':
return Icons.picture_as_pdf;
case 'jpg':
case 'jpeg':
case 'png':
return Icons.image;
default:
return Icons.insert_drive_file;
}
}
void _handleDoubleClickOnFile(BuildContext context, AttachmentModel 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),
),
),
),
);
}
2026-04-11 12:40:03 +02:00
}