From 2aab70aec5a559fe0e83ff1b87af7a8438f07803 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Tue, 12 May 2026 11:14:48 +0200 Subject: [PATCH] sistemati ticket --- .../shared_forms/customer_section.dart | 150 ++++++++++++++++- .../widgets/shared_forms/model_section.dart | 7 +- .../ui/latest_store_operations_card.dart | 2 +- .../blocs/operation_form_cubit.dart | 12 +- .../operations/models/operation_model.dart | 17 +- .../operations/ui/operation_form_screen.dart | 45 +++-- .../operations/ui/operation_list_screen.dart | 2 +- .../tickets/blocs/ticket_form_cubit.dart | 19 ++- lib/features/tickets/models/ticket_model.dart | 27 ++- .../tickets/ui/ticket_form_screen.dart | 159 +++++++++++++++--- .../tickets/ui/ticket_list_screen.dart | 2 +- .../tickets/utils/ticket_pdf_service.dart | 6 +- pubspec.lock | 10 +- pubspec.yaml | 4 +- 14 files changed, 367 insertions(+), 95 deletions(-) diff --git a/lib/core/widgets/shared_forms/customer_section.dart b/lib/core/widgets/shared_forms/customer_section.dart index d2d75ad..271bdd5 100644 --- a/lib/core/widgets/shared_forms/customer_section.dart +++ b/lib/core/widgets/shared_forms/customer_section.dart @@ -1,24 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/routes/routes.dart'; import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/quick_customer_dialog.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:go_router/go_router.dart'; +import 'package:url_launcher/url_launcher.dart'; class SharedCustomerSection extends StatelessWidget { - final String? customerId; - final String? customerName; + final CustomerModel? customer; final ValueChanged onCustomerSelected; const SharedCustomerSection({ super.key, - this.customerId, - this.customerName, + this.customer, required this.onCustomerSelected, }); @override Widget build(BuildContext context) { - final hasCustomer = customerId != null && customerId!.isNotEmpty; + final hasCustomer = customer != null && customer!.id!.isNotEmpty; final theme = Theme.of(context); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -47,7 +50,7 @@ class SharedCustomerSection extends StatelessWidget { const SizedBox(width: 12), Expanded( child: Text( - hasCustomer ? customerName! : 'Seleziona Cliente *', + hasCustomer ? customer!.name : 'Seleziona Cliente *', style: TextStyle( fontWeight: hasCustomer ? FontWeight.bold @@ -57,10 +60,145 @@ class SharedCustomerSection extends StatelessWidget { ), ), const Icon(Icons.search), + + if (hasCustomer) ...[ + const SizedBox(width: 12), + IconButton( + onPressed: () => context.pushNamed( + Routes.customerForm, + pathParameters: {'id': customer!.id!}, + extra: customer, + ), + icon: const Icon(Icons.edit), + ), + ], ], ), ), ), + if (hasCustomer && + (customer!.phoneNumber.isNotEmpty || + customer!.email.isNotEmpty)) ...[ + const SizedBox(height: 12), // Un po' più di respiro dal box sopra + // Mettiamo i contatti in un Container con un po' di stile per farli sembrare una "Contact Card" integrata + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.withValues(alpha: 0.05), // Sfondo leggerissimo + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.grey.withValues(alpha: 0.2)), + ), + child: Column( + children: [ + // --- RIGA TELEFONO --- + if (customer!.phoneNumber.isNotEmpty) + Row( + children: [ + // Usiamo i pulsanti "Small" per non occupare troppo spazio verticale + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () => launchUrl( + Uri.parse('https://wa.me/39${customer!.phoneNumber}'), + ), + icon: const FaIcon( + FontAwesomeIcons.whatsapp, + color: Colors.green, + size: 20, + ), + tooltip: 'Invia WhatsApp', + ), + const SizedBox(width: 8), + Expanded( + // Expanded evita l'overflow se il numero è assurdamente lungo + child: SelectableText( + customer!.phoneNumber, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () { + Clipboard.setData( + ClipboardData(text: customer!.phoneNumber), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Telefono copiato!'), + duration: Duration(seconds: 2), + ), + ); + }, + icon: const Icon( + Icons.copy, + size: 18, + color: Colors.grey, + ), + tooltip: 'Copia', + ), + ], + ), + + // Sezione divisoria se ci sono entrambi + if (customer!.phoneNumber.isNotEmpty && + customer!.email.isNotEmpty) + const Divider(height: 8, thickness: 0.5), + + // --- RIGA EMAIL --- + if (customer!.email.isNotEmpty) + Row( + children: [ + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () => launchUrl( + Uri.parse('mailto:${customer!.email}'), + ), // Rimosso il // dopo mailto:, è più sicuro + icon: const FaIcon( + FontAwesomeIcons.envelope, + color: Colors.blue, + size: 20, + ), + tooltip: 'Invia Email', + ), + const SizedBox(width: 8), + Expanded( + // L'Expanded è vitale per le email che possono essere lunghissime + child: SelectableText( + customer!.email, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + IconButton( + visualDensity: VisualDensity.compact, + onPressed: () { + Clipboard.setData( + ClipboardData(text: customer!.email), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Email copiata!'), + duration: Duration(seconds: 2), + ), + ); + }, + icon: const Icon( + Icons.copy, + size: 18, + color: Colors.grey, + ), + tooltip: 'Copia', + ), + ], + ), + ], + ), + ), + ], ], ); } diff --git a/lib/core/widgets/shared_forms/model_section.dart b/lib/core/widgets/shared_forms/model_section.dart index 1bdc70f..7aaa097 100644 --- a/lib/core/widgets/shared_forms/model_section.dart +++ b/lib/core/widgets/shared_forms/model_section.dart @@ -7,6 +7,8 @@ class SharedModelSection extends StatelessWidget { final String? modelId; final String? modelName; final String label; + final Color? backgroundColor; + final Color? borderColor; // Usiamo una callback che passa direttamente ID e Nome // così non dobbiamo preoccuparci di importare la classe esatta del modello ovunque @@ -18,6 +20,8 @@ class SharedModelSection extends StatelessWidget { required this.modelName, required this.onModelSelected, this.label = 'Seleziona Modello', + this.backgroundColor, + this.borderColor, }); @override @@ -26,6 +30,7 @@ class SharedModelSection extends StatelessWidget { final hasModel = modelId != null && modelId!.isNotEmpty; return ListTile( + tileColor: backgroundColor, title: Text(label), subtitle: Text( hasModel ? modelName! : 'Nessun modello selezionato', @@ -36,7 +41,7 @@ class SharedModelSection extends StatelessWidget { ), trailing: const Icon(Icons.arrow_drop_down), shape: RoundedRectangleBorder( - side: BorderSide(color: theme.dividerColor), + side: BorderSide(color: borderColor ?? theme.dividerColor), borderRadius: BorderRadius.circular(8), ), onTap: () => _showModelModal(context), diff --git a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart index c86aca1..8d037c1 100644 --- a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart +++ b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart @@ -151,7 +151,7 @@ class _LatestOperationsCardContent extends StatelessWidget { Expanded( flex: 5, child: Text( - operation.customerDisplayName ?? + operation.customer?.name ?? 'Cliente sconosciuto', style: TextStyle( fontWeight: FontWeight.w700, diff --git a/lib/features/operations/blocs/operation_form_cubit.dart b/lib/features/operations/blocs/operation_form_cubit.dart index 50a7318..f07dd05 100644 --- a/lib/features/operations/blocs/operation_form_cubit.dart +++ b/lib/features/operations/blocs/operation_form_cubit.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:get_it/get_it.dart'; @@ -90,7 +91,7 @@ class OperationFormCubit extends Cubit { storeDisplayName: current.storeDisplayName, batchUuid: current.batchUuid, // MANTIENE IL COLLEGAMENTO customerId: current.customerId, // MANTIENE IL CLIENTE - customerDisplayName: current.customerDisplayName, + customer: current.customer, status: OperationStatus.draft, createdAt: DateTime.now(), ), @@ -178,8 +179,7 @@ class OperationFormCubit extends Cubit { // --- GESTIONE DEI CAMPI IN TEMPO REALE --- void updateFields({ - String? customerId, - String? customerDisplayName, + CustomerModel? customer, String? reference, String? note, String? type, @@ -211,10 +211,8 @@ class OperationFormCubit extends Cubit { if (quantity != null && quantity > 0) newQuantity = quantity; final updated = current.copyWith( - customerId: - customerId ?? - current.customerId, // Se non passo customerId, tengo il vecchio - customerDisplayName: customerDisplayName ?? current.customerDisplayName, + customer: customer ?? current.customer, + customerId: customer?.id ?? current.customerId, reference: reference ?? current.reference, note: note ?? current.note, providerId: clearProvider ? null : (providerId ?? current.providerId), diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index e465f3e..df88f7b 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/attachments/models/attachment_model.dart'; +import 'package:flux/features/customers/models/customer_model.dart'; enum OperationStatus { success('success', 'OK'), @@ -45,7 +46,7 @@ class OperationModel extends Equatable { final String? lastCampaignId; final OperationStatus status; final String? customerId; - final String? customerDisplayName; + final CustomerModel? customer; final String reference; // ALLEGATI (Aggiunto) @@ -74,7 +75,7 @@ class OperationModel extends Equatable { this.lastCampaignId, this.status = OperationStatus.draft, this.customerId, - this.customerDisplayName, + this.customer, this.reference = '', this.attachments = const [], }); @@ -102,7 +103,7 @@ class OperationModel extends Equatable { String? lastCampaignId, OperationStatus? status, String? customerId, - String? customerDisplayName, + CustomerModel? customer, String? reference, List? attachments, }) => OperationModel( @@ -128,7 +129,7 @@ class OperationModel extends Equatable { lastCampaignId: lastCampaignId ?? this.lastCampaignId, status: status ?? this.status, customerId: customerId ?? this.customerId, - customerDisplayName: customerDisplayName ?? this.customerDisplayName, + customer: customer ?? this.customer, reference: reference ?? this.reference, attachments: attachments ?? this.attachments, ); @@ -157,7 +158,7 @@ class OperationModel extends Equatable { lastCampaignId, status, customerId, - customerDisplayName, + customer, reference, attachments, ]; @@ -207,9 +208,11 @@ class OperationModel extends Equatable { lastCampaignId: map['last_campaign_id'] as String?, status: OperationStatus.fromString(map['status'] ?? 'draft'), - customerId: map['customer_id'] as String?, - customerDisplayName: (map['customer']?['name'] as String?)?.myFormat(), + + customer: map['customer'] != null + ? CustomerModel.fromMap(map['customer'] as Map) + : null, attachments: (map['attachment'] as List?) diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index 86a0f4b..3e69fe5 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -265,23 +265,6 @@ class _OperationFormScreenState extends State { padding: const EdgeInsets.all(16.0), child: Row( children: [ - Expanded( - flex: 1, - child: OutlinedButton( - onPressed: state.status == OperationFormStatus.saving - ? null - : () => _saveOperation( - keepAdding: true, - targetStatus: - displayStatus, // <-- Usiamo lo stato selezionato nel form! - ), - child: const Text( - 'Salva e Aggiungi Altro', - textAlign: TextAlign.center, - ), - ), - ), - const SizedBox(width: 12), Expanded( flex: 1, child: ElevatedButton( @@ -317,6 +300,24 @@ class _OperationFormScreenState extends State { : const Text('Salva ed Esci'), ), ), + + const SizedBox(width: 12), + Expanded( + flex: 1, + child: OutlinedButton( + onPressed: state.status == OperationFormStatus.saving + ? null + : () => _saveOperation( + keepAdding: true, + targetStatus: + displayStatus, // <-- Usiamo lo stato selezionato nel form! + ), + child: const Text( + 'Salva e Aggiungi Altro', + textAlign: TextAlign.center, + ), + ), + ), ], ), ), @@ -466,13 +467,9 @@ class _OperationFormScreenState extends State { Widget _buildCustomerSection(OperationFormState state) { return SharedCustomerSection( - customerId: state.operation.customerId, - customerName: state.operation.customerDisplayName, + customer: state.operation.customer, onCustomerSelected: (customer) { - context.read().updateFields( - customerId: customer.id, - customerDisplayName: customer.name, - ); + context.read().updateFields(customer: customer); }, ); } @@ -559,7 +556,7 @@ class _OperationFormScreenState extends State { return SharedAttachmentsSection( parentType: AttachmentParentType.operation, parentId: state.operation.id, - titleForUpload: state.operation.customerDisplayName ?? 'Nuova pratica', + titleForUpload: state.operation.customer?.name ?? 'Nuova pratica', onGenerateIdForQr: _generateIdForQr, ); } diff --git a/lib/features/operations/ui/operation_list_screen.dart b/lib/features/operations/ui/operation_list_screen.dart index 3d091ff..a172987 100644 --- a/lib/features/operations/ui/operation_list_screen.dart +++ b/lib/features/operations/ui/operation_list_screen.dart @@ -130,7 +130,7 @@ class _OperationListScreenState extends State { children: [ Expanded( child: Text( - operation.customerDisplayName ?? "Cliente sconosciuto", + operation.customer?.name ?? "Cliente sconosciuto", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, diff --git a/lib/features/tickets/blocs/ticket_form_cubit.dart b/lib/features/tickets/blocs/ticket_form_cubit.dart index 8a3a028..8014bbb 100644 --- a/lib/features/tickets/blocs/ticket_form_cubit.dart +++ b/lib/features/tickets/blocs/ticket_form_cubit.dart @@ -72,9 +72,9 @@ class TicketFormCubit extends Cubit { state.copyWith( ticket: state.ticket.copyWith( customerId: customer.id, - customerName: customer.name, - alternativePhoneNumber: customer.phoneNumber, - customerEmail: customer.email, + customer: customer, + alternativePhoneNumber: + state.ticket.alternativePhoneNumber ?? customer.phoneNumber, ), ), ); @@ -92,6 +92,17 @@ class TicketFormCubit extends Cubit { ); } + void updateSourceModel({required String modelId, required String modelName}) { + emit( + state.copyWith( + ticket: state.ticket.copyWith( + sourceModelId: modelId, + sourceModelName: modelName, + ), + ), + ); + } + void updateCreator({required String staffId, required String staffName}) { emit( state.copyWith( @@ -109,6 +120,7 @@ class TicketFormCubit extends Cubit { TicketStatus? status, String? request, String? targetSn, + String? sourceSn, String? alternativePhoneNumber, bool? hasCourtesyDevice, String? includedAccessories, @@ -126,6 +138,7 @@ class TicketFormCubit extends Cubit { ticketStatus: status ?? state.ticket.ticketStatus, request: request ?? state.ticket.request, targetSn: targetSn ?? state.ticket.targetSn, + sourceSn: sourceSn ?? state.ticket.sourceSn, alternativePhoneNumber: alternativePhoneNumber ?? state.ticket.alternativePhoneNumber, hasCourtesyDevice: diff --git a/lib/features/tickets/models/ticket_model.dart b/lib/features/tickets/models/ticket_model.dart index 55bb53d..523d4b6 100644 --- a/lib/features/tickets/models/ticket_model.dart +++ b/lib/features/tickets/models/ticket_model.dart @@ -1,12 +1,13 @@ import 'package:equatable/equatable.dart'; import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/features/customers/models/customer_model.dart'; /// Enum per il tipo di ticket enum TicketType { repair('repair', 'Riparazione'), - softwareSetup('software_setup', 'Setup software'), - dataTransfer('data_transfer', 'Trasferimento dati'), - operationTicket('operation_ticket', 'Ticket di operazione'), + softwareSetup('software_setup', 'Impost. software'), + dataTransfer('data_transfer', 'Trasf. dati'), + operationTicket('operation_ticket', 'Ticket operazione'), other('other', 'Altro'); final String value; @@ -106,8 +107,7 @@ class TicketModel extends Equatable { final DateTime? estimatedDeliveryAt; final TicketResult? ticketResult; final String? resolutionNotes; - final String? customerName; - final String? customerEmail; + final CustomerModel? customer; final String? targetModelName; final String? sourceModelName; final String? createdById; @@ -142,8 +142,7 @@ class TicketModel extends Equatable { this.estimatedDeliveryAt, this.ticketResult, this.resolutionNotes, - this.customerName, - this.customerEmail, + this.customer, this.targetModelName, this.sourceModelName, this.createdById, @@ -193,8 +192,7 @@ class TicketModel extends Equatable { DateTime? estimatedDeliveryAt, TicketResult? ticketResult, String? resolutionNotes, - String? customerName, - String? customerEmail, + CustomerModel? customer, String? targetModelName, String? sourceModelName, String? createdById, @@ -230,8 +228,7 @@ class TicketModel extends Equatable { estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt, ticketResult: ticketResult ?? this.ticketResult, resolutionNotes: resolutionNotes ?? this.resolutionNotes, - customerName: customerName ?? this.customerName, - customerEmail: customerEmail ?? this.customerEmail, + customer: customer ?? this.customer, targetModelName: targetModelName ?? this.targetModelName, sourceModelName: sourceModelName ?? this.sourceModelName, createdById: createdById ?? this.createdById, @@ -279,8 +276,9 @@ class TicketModel extends Equatable { : null, ticketResult: TicketResult.fromString(map['ticket_result'] as String?), resolutionNotes: map['resolution_notes'] as String?, - customerName: (map['customer']?['name'] as String?).myFormat(), - customerEmail: (map['customer']?['email'] as String?).myFormat(), + customer: map['customer'] != null + ? CustomerModel.fromMap(map['customer'] as Map) + : null, targetModelName: (map['target_model']?['name_with_brand'] as String?) ?.myFormat(), sourceModelName: (map['source_model']?['name_with_brand'] as String?) @@ -354,8 +352,7 @@ class TicketModel extends Equatable { ticketResult, resolutionNotes, includedAccessories, - customerName, - customerEmail, + customer, targetModelName, sourceModelName, createdById, diff --git a/lib/features/tickets/ui/ticket_form_screen.dart b/lib/features/tickets/ui/ticket_form_screen.dart index 9510c57..22a7449 100644 --- a/lib/features/tickets/ui/ticket_form_screen.dart +++ b/lib/features/tickets/ui/ticket_form_screen.dart @@ -15,7 +15,6 @@ import 'package:flux/core/widgets/shared_forms/staff_section.dart'; import 'package:flux/features/tickets/models/ticket_status_extension.dart'; import 'package:flux/features/tickets/utils/ticket_pdf_service.dart'; import 'package:get_it/get_it.dart'; -import 'package:pdf/pdf.dart'; import 'package:printing/printing.dart'; class TicketFormScreen extends StatefulWidget { @@ -32,7 +31,8 @@ class _TicketFormScreenState extends State { final _formKey = GlobalKey(); final _altPhoneCtrl = TextEditingController(); - final _serialCtrl = TextEditingController(); + final _targetSerialCtrl = TextEditingController(); + final _sourceSerialCtrl = TextEditingController(); final _requestCtrl = TextEditingController(); final _accessoriesCtrl = TextEditingController(); final _publicNotesCtrl = TextEditingController(); @@ -54,7 +54,8 @@ class _TicketFormScreenState extends State { @override void dispose() { _altPhoneCtrl.dispose(); - _serialCtrl.dispose(); + _targetSerialCtrl.dispose(); + _sourceSerialCtrl.dispose(); _requestCtrl.dispose(); _accessoriesCtrl.dispose(); _publicNotesCtrl.dispose(); @@ -68,7 +69,12 @@ class _TicketFormScreenState extends State { if (_altPhoneCtrl.text.isEmpty) { _altPhoneCtrl.text = model.alternativePhoneNumber ?? ''; } - if (_serialCtrl.text.isEmpty) _serialCtrl.text = model.targetSn ?? ''; + if (_targetSerialCtrl.text.isEmpty) { + _targetSerialCtrl.text = model.targetSn ?? ''; + } + if (_sourceSerialCtrl.text.isEmpty) { + _sourceSerialCtrl.text = model.sourceSn ?? ''; + } if (_requestCtrl.text.isEmpty) _requestCtrl.text = model.request; if (_accessoriesCtrl.text.isEmpty) { _accessoriesCtrl.text = model.includedAccessories ?? ''; @@ -91,7 +97,8 @@ class _TicketFormScreenState extends State { void _flushControllersToCubit() { context.read().updateFields( alternativePhoneNumber: _altPhoneCtrl.text, - targetSn: _serialCtrl.text, + targetSn: _targetSerialCtrl.text, + sourceSn: _sourceSerialCtrl.text, request: _requestCtrl.text, includedAccessories: _accessoriesCtrl.text, publicNotes: _publicNotesCtrl.text, @@ -231,7 +238,7 @@ class _TicketFormScreenState extends State { _ActionButton( icon: Icons.email, label: "Invia Email", - onTap: ticket.customerEmail != null ? () {} : null, + onTap: ticket.customer!.email.isNotEmpty ? () {} : null, ), _ActionButton( icon: Icons.close, @@ -362,6 +369,7 @@ class _TicketFormScreenState extends State { child: const Text('Ricevuta'), ), ), + const SizedBox(width: 12), Expanded( flex: 1, child: ElevatedButton( @@ -401,17 +409,17 @@ class _TicketFormScreenState extends State { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Expanded(child: Column(children: [_cardAnagrafica(ticket)])), + const SizedBox(width: 24), Expanded( child: Column( - children: [_cardAnagrafica(ticket), _cardDispositivo(ticket)], + children: [_cardDettagli(ticket), _cardCosti(ticket)], ), ), const SizedBox(width: 24), - Expanded(child: Column(children: [_cardDettagli(ticket)])), - const SizedBox(width: 24), Expanded( child: Column( - children: [_cardCosti(ticket), _cardAssegnazione(ticket)], + children: [_cardDispositivi(ticket), _cardAssegnazione(ticket)], ), ), ], @@ -425,7 +433,7 @@ class _TicketFormScreenState extends State { child: Column( children: [ _cardAnagrafica(ticket), - _cardDispositivo(ticket), + _cardDispositivi(ticket), _cardAssegnazione(ticket), ], ), @@ -444,7 +452,7 @@ class _TicketFormScreenState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _cardAnagrafica(ticket), - _cardDispositivo(ticket), + _cardDispositivi(ticket), _cardDettagli(ticket), _cardCosti(ticket), _cardAssegnazione(ticket), @@ -471,8 +479,7 @@ class _TicketFormScreenState extends State { ), const Divider(height: 32), SharedCustomerSection( - customerId: ticket.customerId, - customerName: ticket.customerName, + customer: ticket.customer, onCustomerSelected: (customer) => context.read().updateCustomer(customer), ), @@ -488,14 +495,19 @@ class _TicketFormScreenState extends State { ); } - Widget _cardDispositivo(TicketModel ticket) { + Widget _cardDispositivi(TicketModel ticket) { + final bool isDataTransfer = ticket.ticketType == TicketType.dataTransfer; + return _buildCard( - title: 'Dispositivo', + title: isDataTransfer ? 'Dispositivi' : 'Dispositivo', icon: Icons.devices, themeColor: Colors.deepOrange, children: [ + // --- DISPOSITIVO TARGET (Nuovo/Ricevente) --- SharedModelSection( - label: 'Modello da Riparare', + label: isDataTransfer + ? 'Dispositivo Target (Nuovo/Ricevente)' + : 'Modello da Riparare', modelId: ticket.targetModelId, modelName: ticket.targetModelName, onModelSelected: (id, name) => context @@ -504,12 +516,108 @@ class _TicketFormScreenState extends State { ), const SizedBox(height: 16), TextFormField( - controller: _serialCtrl, + controller: _targetSerialCtrl, // Controller per il seriale TARGET decoration: const InputDecoration( labelText: 'Seriale / IMEI', prefixIcon: Icon(Icons.qr_code), ), ), + + // --- DISPOSITIVO SORGENTE (Animato per Passaggio Dati) --- + AnimatedSize( + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + alignment: Alignment.topCenter, + child: isDataTransfer + ? Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + // Bordo trasparente e delicato per definire la card + border: Border.all( + color: Colors.orange.shade300.withValues(alpha: 0.2), + ), + borderRadius: BorderRadius.circular(8), + // SFONDO RIMOSSO: vedi direttamente il tema scuro sotto! + // color: Colors.transparent, // opzionale + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.devices_fold, + color: Colors.orange.shade700, + ), + const SizedBox(width: 12), + Text( + 'Dispositivo Sorgente', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.orange.shade900, + ), + ), + ], + ), + const SizedBox(height: 16), + + // LA SHARED SECTION "SOFT" + SharedModelSection( + label: 'Modello Sorgente (Da cui copiare)', + modelId: ticket.sourceModelId, + modelName: ticket.sourceModelName, + // Sfondo quasi trasparente per non appesantire + backgroundColor: Colors.white.withValues(alpha: 0.1), + // Bordo delicato + borderColor: Colors.orange.shade300.withValues( + alpha: 0.2, + ), + onModelSelected: (id, name) => context + .read() + .updateSourceModel(modelId: id, modelName: name), + ), + + const SizedBox(height: 16), + TextFormField( + controller: _sourceSerialCtrl, + decoration: InputDecoration( + labelText: 'Seriale / IMEI Sorgente', + prefixIcon: Icon( + Icons.qr_code, + color: Colors.orange.shade700.withValues( + alpha: 0.7, + ), + ), + // Usiamo lo stesso riempimento tenue per coerenza + fillColor: Colors.white.withValues(alpha: 0.1), + filled: true, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.orange.shade300.withValues( + alpha: 0.2, + ), + ), + borderRadius: BorderRadius.circular(8), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.orange.shade500.withValues( + alpha: 0.5, + ), + ), + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ], + ), + ), + ) + : const SizedBox.shrink(), + ), ], ); } @@ -529,11 +637,16 @@ class _TicketFormScreenState extends State { labelText: 'Tipo Lavorazione', ), items: TicketType.values - .map((t) => DropdownMenuItem(value: t, child: Text(t.name))) + .map( + (t) => DropdownMenuItem( + value: t, + child: Text(t.displayValue), + ), + ) .toList(), - onChanged: (val) => context - .read() - .updateFields(ticketType: val), + onChanged: (val) { + context.read().updateFields(ticketType: val); + }, ), ), const SizedBox(width: 16), @@ -657,7 +770,7 @@ class _TicketFormScreenState extends State { const Divider(height: 32), // ECCO LA MAGIA: SharedFilesSection( - titleNameForUpload: ticket.customerName ?? 'Nuovo Ticket', + titleNameForUpload: ticket.customer?.name ?? 'Nuovo Ticket', onGenerateIdForQr: _generateIdForQr, ), /* SharedAttachmentsSection( diff --git a/lib/features/tickets/ui/ticket_list_screen.dart b/lib/features/tickets/ui/ticket_list_screen.dart index 7d2d51d..4df83be 100644 --- a/lib/features/tickets/ui/ticket_list_screen.dart +++ b/lib/features/tickets/ui/ticket_list_screen.dart @@ -219,7 +219,7 @@ class _TicketCard extends StatelessWidget { children: [ Expanded( child: Text( - ticket.customerName ?? 'Cliente Sconosciuto', + ticket.customer?.name ?? 'Cliente Sconosciuto', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, diff --git a/lib/features/tickets/utils/ticket_pdf_service.dart b/lib/features/tickets/utils/ticket_pdf_service.dart index 33bbb72..5a48fda 100644 --- a/lib/features/tickets/utils/ticket_pdf_service.dart +++ b/lib/features/tickets/utils/ticket_pdf_service.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:get_it/get_it.dart'; import 'package:pdf/pdf.dart'; @@ -152,7 +150,7 @@ class TicketPdfService { pw.Expanded( child: _infoBlock( "CLIENTE", - ticket.customerName ?? 'Cliente Sconosciuto', + ticket.customer?.name ?? 'Cliente Sconosciuto', font, boldFont, ), @@ -317,7 +315,7 @@ class TicketPdfService { style: pw.TextStyle(font: boldFont, fontSize: 10), ), pw.Text( - ticket.customerName ?? 'Cliente sconosciuto', + ticket.customer?.name ?? 'Cliente sconosciuto', style: pw.TextStyle(font: font, fontSize: 9), ), pw.Text( diff --git a/pubspec.lock b/pubspec.lock index 35de733..b42087c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -309,6 +309,14 @@ packages: description: flutter source: sdk version: "0.0.0" + font_awesome_flutter: + dependency: "direct main" + description: + name: font_awesome_flutter + sha256: "09dcde8ab90ffae1a7d65ff2ef96fc62a17ad9d0ce7c127b317ded676b0d5935" + url: "https://pub.dev" + source: hosted + version: "11.0.0" functions_client: dependency: transitive description: @@ -1043,7 +1051,7 @@ packages: source: hosted version: "1.1.0" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 diff --git a/pubspec.yaml b/pubspec.yaml index 2d3071c..a2bff26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,9 @@ dependencies: uuid: ^4.5.3 pdf: ^3.12.0 universal_io: ^2.3.1 - printing: ^5.13.1 + url_launcher: ^6.3.2 + printing: ^5.14.3 + font_awesome_flutter: ^11.0.0 dev_dependencies: flutter_test: