sistemati ticket

This commit is contained in:
2026-05-12 11:14:48 +02:00
parent 57061da20d
commit 2aab70aec5
14 changed files with 367 additions and 95 deletions

View File

@@ -1,24 +1,27 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.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/blocs/customers_cubit.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/quick_customer_dialog.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 { class SharedCustomerSection extends StatelessWidget {
final String? customerId; final CustomerModel? customer;
final String? customerName;
final ValueChanged<CustomerModel> onCustomerSelected; final ValueChanged<CustomerModel> onCustomerSelected;
const SharedCustomerSection({ const SharedCustomerSection({
super.key, super.key,
this.customerId, this.customer,
this.customerName,
required this.onCustomerSelected, required this.onCustomerSelected,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hasCustomer = customerId != null && customerId!.isNotEmpty; final hasCustomer = customer != null && customer!.id!.isNotEmpty;
final theme = Theme.of(context); final theme = Theme.of(context);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -47,7 +50,7 @@ class SharedCustomerSection extends StatelessWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
hasCustomer ? customerName! : 'Seleziona Cliente *', hasCustomer ? customer!.name : 'Seleziona Cliente *',
style: TextStyle( style: TextStyle(
fontWeight: hasCustomer fontWeight: hasCustomer
? FontWeight.bold ? FontWeight.bold
@@ -57,10 +60,145 @@ class SharedCustomerSection extends StatelessWidget {
), ),
), ),
const Icon(Icons.search), 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',
),
],
),
],
),
),
],
], ],
); );
} }

View File

@@ -7,6 +7,8 @@ class SharedModelSection extends StatelessWidget {
final String? modelId; final String? modelId;
final String? modelName; final String? modelName;
final String label; final String label;
final Color? backgroundColor;
final Color? borderColor;
// Usiamo una callback che passa direttamente ID e Nome // Usiamo una callback che passa direttamente ID e Nome
// così non dobbiamo preoccuparci di importare la classe esatta del modello ovunque // 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.modelName,
required this.onModelSelected, required this.onModelSelected,
this.label = 'Seleziona Modello', this.label = 'Seleziona Modello',
this.backgroundColor,
this.borderColor,
}); });
@override @override
@@ -26,6 +30,7 @@ class SharedModelSection extends StatelessWidget {
final hasModel = modelId != null && modelId!.isNotEmpty; final hasModel = modelId != null && modelId!.isNotEmpty;
return ListTile( return ListTile(
tileColor: backgroundColor,
title: Text(label), title: Text(label),
subtitle: Text( subtitle: Text(
hasModel ? modelName! : 'Nessun modello selezionato', hasModel ? modelName! : 'Nessun modello selezionato',
@@ -36,7 +41,7 @@ class SharedModelSection extends StatelessWidget {
), ),
trailing: const Icon(Icons.arrow_drop_down), trailing: const Icon(Icons.arrow_drop_down),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
side: BorderSide(color: theme.dividerColor), side: BorderSide(color: borderColor ?? theme.dividerColor),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
onTap: () => _showModelModal(context), onTap: () => _showModelModal(context),

View File

@@ -151,7 +151,7 @@ class _LatestOperationsCardContent extends StatelessWidget {
Expanded( Expanded(
flex: 5, flex: 5,
child: Text( child: Text(
operation.customerDisplayName ?? operation.customer?.name ??
'Cliente sconosciuto', 'Cliente sconosciuto',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,

View File

@@ -1,6 +1,7 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.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/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/data/operations_repository.dart';
import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@@ -90,7 +91,7 @@ class OperationFormCubit extends Cubit<OperationFormState> {
storeDisplayName: current.storeDisplayName, storeDisplayName: current.storeDisplayName,
batchUuid: current.batchUuid, // MANTIENE IL COLLEGAMENTO batchUuid: current.batchUuid, // MANTIENE IL COLLEGAMENTO
customerId: current.customerId, // MANTIENE IL CLIENTE customerId: current.customerId, // MANTIENE IL CLIENTE
customerDisplayName: current.customerDisplayName, customer: current.customer,
status: OperationStatus.draft, status: OperationStatus.draft,
createdAt: DateTime.now(), createdAt: DateTime.now(),
), ),
@@ -178,8 +179,7 @@ class OperationFormCubit extends Cubit<OperationFormState> {
// --- GESTIONE DEI CAMPI IN TEMPO REALE --- // --- GESTIONE DEI CAMPI IN TEMPO REALE ---
void updateFields({ void updateFields({
String? customerId, CustomerModel? customer,
String? customerDisplayName,
String? reference, String? reference,
String? note, String? note,
String? type, String? type,
@@ -211,10 +211,8 @@ class OperationFormCubit extends Cubit<OperationFormState> {
if (quantity != null && quantity > 0) newQuantity = quantity; if (quantity != null && quantity > 0) newQuantity = quantity;
final updated = current.copyWith( final updated = current.copyWith(
customerId: customer: customer ?? current.customer,
customerId ?? customerId: customer?.id ?? current.customerId,
current.customerId, // Se non passo customerId, tengo il vecchio
customerDisplayName: customerDisplayName ?? current.customerDisplayName,
reference: reference ?? current.reference, reference: reference ?? current.reference,
note: note ?? current.note, note: note ?? current.note,
providerId: clearProvider ? null : (providerId ?? current.providerId), providerId: clearProvider ? null : (providerId ?? current.providerId),

View File

@@ -1,6 +1,7 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/extensions.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';
enum OperationStatus { enum OperationStatus {
success('success', 'OK'), success('success', 'OK'),
@@ -45,7 +46,7 @@ class OperationModel extends Equatable {
final String? lastCampaignId; final String? lastCampaignId;
final OperationStatus status; final OperationStatus status;
final String? customerId; final String? customerId;
final String? customerDisplayName; final CustomerModel? customer;
final String reference; final String reference;
// ALLEGATI (Aggiunto) // ALLEGATI (Aggiunto)
@@ -74,7 +75,7 @@ class OperationModel extends Equatable {
this.lastCampaignId, this.lastCampaignId,
this.status = OperationStatus.draft, this.status = OperationStatus.draft,
this.customerId, this.customerId,
this.customerDisplayName, this.customer,
this.reference = '', this.reference = '',
this.attachments = const [], this.attachments = const [],
}); });
@@ -102,7 +103,7 @@ class OperationModel extends Equatable {
String? lastCampaignId, String? lastCampaignId,
OperationStatus? status, OperationStatus? status,
String? customerId, String? customerId,
String? customerDisplayName, CustomerModel? customer,
String? reference, String? reference,
List<AttachmentModel>? attachments, List<AttachmentModel>? attachments,
}) => OperationModel( }) => OperationModel(
@@ -128,7 +129,7 @@ class OperationModel extends Equatable {
lastCampaignId: lastCampaignId ?? this.lastCampaignId, lastCampaignId: lastCampaignId ?? this.lastCampaignId,
status: status ?? this.status, status: status ?? this.status,
customerId: customerId ?? this.customerId, customerId: customerId ?? this.customerId,
customerDisplayName: customerDisplayName ?? this.customerDisplayName, customer: customer ?? this.customer,
reference: reference ?? this.reference, reference: reference ?? this.reference,
attachments: attachments ?? this.attachments, attachments: attachments ?? this.attachments,
); );
@@ -157,7 +158,7 @@ class OperationModel extends Equatable {
lastCampaignId, lastCampaignId,
status, status,
customerId, customerId,
customerDisplayName, customer,
reference, reference,
attachments, attachments,
]; ];
@@ -207,9 +208,11 @@ class OperationModel extends Equatable {
lastCampaignId: map['last_campaign_id'] as String?, lastCampaignId: map['last_campaign_id'] as String?,
status: OperationStatus.fromString(map['status'] ?? 'draft'), status: OperationStatus.fromString(map['status'] ?? 'draft'),
customerId: map['customer_id'] as String?, customerId: map['customer_id'] as String?,
customerDisplayName: (map['customer']?['name'] as String?)?.myFormat(),
customer: map['customer'] != null
? CustomerModel.fromMap(map['customer'] as Map<String, dynamic>)
: null,
attachments: attachments:
(map['attachment'] as List?) (map['attachment'] as List?)

View File

@@ -265,23 +265,6 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Row( child: Row(
children: [ 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( Expanded(
flex: 1, flex: 1,
child: ElevatedButton( child: ElevatedButton(
@@ -317,6 +300,24 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
: const Text('Salva ed Esci'), : 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<OperationFormScreen> {
Widget _buildCustomerSection(OperationFormState state) { Widget _buildCustomerSection(OperationFormState state) {
return SharedCustomerSection( return SharedCustomerSection(
customerId: state.operation.customerId, customer: state.operation.customer,
customerName: state.operation.customerDisplayName,
onCustomerSelected: (customer) { onCustomerSelected: (customer) {
context.read<OperationFormCubit>().updateFields( context.read<OperationFormCubit>().updateFields(customer: customer);
customerId: customer.id,
customerDisplayName: customer.name,
);
}, },
); );
} }
@@ -559,7 +556,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
return SharedAttachmentsSection( return SharedAttachmentsSection(
parentType: AttachmentParentType.operation, parentType: AttachmentParentType.operation,
parentId: state.operation.id, parentId: state.operation.id,
titleForUpload: state.operation.customerDisplayName ?? 'Nuova pratica', titleForUpload: state.operation.customer?.name ?? 'Nuova pratica',
onGenerateIdForQr: _generateIdForQr, onGenerateIdForQr: _generateIdForQr,
); );
} }

View File

@@ -130,7 +130,7 @@ class _OperationListScreenState extends State<OperationListScreen> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
operation.customerDisplayName ?? "Cliente sconosciuto", operation.customer?.name ?? "Cliente sconosciuto",
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,

View File

@@ -72,9 +72,9 @@ class TicketFormCubit extends Cubit<TicketFormState> {
state.copyWith( state.copyWith(
ticket: state.ticket.copyWith( ticket: state.ticket.copyWith(
customerId: customer.id, customerId: customer.id,
customerName: customer.name, customer: customer,
alternativePhoneNumber: customer.phoneNumber, alternativePhoneNumber:
customerEmail: customer.email, state.ticket.alternativePhoneNumber ?? customer.phoneNumber,
), ),
), ),
); );
@@ -92,6 +92,17 @@ class TicketFormCubit extends Cubit<TicketFormState> {
); );
} }
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}) { void updateCreator({required String staffId, required String staffName}) {
emit( emit(
state.copyWith( state.copyWith(
@@ -109,6 +120,7 @@ class TicketFormCubit extends Cubit<TicketFormState> {
TicketStatus? status, TicketStatus? status,
String? request, String? request,
String? targetSn, String? targetSn,
String? sourceSn,
String? alternativePhoneNumber, String? alternativePhoneNumber,
bool? hasCourtesyDevice, bool? hasCourtesyDevice,
String? includedAccessories, String? includedAccessories,
@@ -126,6 +138,7 @@ class TicketFormCubit extends Cubit<TicketFormState> {
ticketStatus: status ?? state.ticket.ticketStatus, ticketStatus: status ?? state.ticket.ticketStatus,
request: request ?? state.ticket.request, request: request ?? state.ticket.request,
targetSn: targetSn ?? state.ticket.targetSn, targetSn: targetSn ?? state.ticket.targetSn,
sourceSn: sourceSn ?? state.ticket.sourceSn,
alternativePhoneNumber: alternativePhoneNumber:
alternativePhoneNumber ?? state.ticket.alternativePhoneNumber, alternativePhoneNumber ?? state.ticket.alternativePhoneNumber,
hasCourtesyDevice: hasCourtesyDevice:

View File

@@ -1,12 +1,13 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/customers/models/customer_model.dart';
/// Enum per il tipo di ticket /// Enum per il tipo di ticket
enum TicketType { enum TicketType {
repair('repair', 'Riparazione'), repair('repair', 'Riparazione'),
softwareSetup('software_setup', 'Setup software'), softwareSetup('software_setup', 'Impost. software'),
dataTransfer('data_transfer', 'Trasferimento dati'), dataTransfer('data_transfer', 'Trasf. dati'),
operationTicket('operation_ticket', 'Ticket di operazione'), operationTicket('operation_ticket', 'Ticket operazione'),
other('other', 'Altro'); other('other', 'Altro');
final String value; final String value;
@@ -106,8 +107,7 @@ class TicketModel extends Equatable {
final DateTime? estimatedDeliveryAt; final DateTime? estimatedDeliveryAt;
final TicketResult? ticketResult; final TicketResult? ticketResult;
final String? resolutionNotes; final String? resolutionNotes;
final String? customerName; final CustomerModel? customer;
final String? customerEmail;
final String? targetModelName; final String? targetModelName;
final String? sourceModelName; final String? sourceModelName;
final String? createdById; final String? createdById;
@@ -142,8 +142,7 @@ class TicketModel extends Equatable {
this.estimatedDeliveryAt, this.estimatedDeliveryAt,
this.ticketResult, this.ticketResult,
this.resolutionNotes, this.resolutionNotes,
this.customerName, this.customer,
this.customerEmail,
this.targetModelName, this.targetModelName,
this.sourceModelName, this.sourceModelName,
this.createdById, this.createdById,
@@ -193,8 +192,7 @@ class TicketModel extends Equatable {
DateTime? estimatedDeliveryAt, DateTime? estimatedDeliveryAt,
TicketResult? ticketResult, TicketResult? ticketResult,
String? resolutionNotes, String? resolutionNotes,
String? customerName, CustomerModel? customer,
String? customerEmail,
String? targetModelName, String? targetModelName,
String? sourceModelName, String? sourceModelName,
String? createdById, String? createdById,
@@ -230,8 +228,7 @@ class TicketModel extends Equatable {
estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt, estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt,
ticketResult: ticketResult ?? this.ticketResult, ticketResult: ticketResult ?? this.ticketResult,
resolutionNotes: resolutionNotes ?? this.resolutionNotes, resolutionNotes: resolutionNotes ?? this.resolutionNotes,
customerName: customerName ?? this.customerName, customer: customer ?? this.customer,
customerEmail: customerEmail ?? this.customerEmail,
targetModelName: targetModelName ?? this.targetModelName, targetModelName: targetModelName ?? this.targetModelName,
sourceModelName: sourceModelName ?? this.sourceModelName, sourceModelName: sourceModelName ?? this.sourceModelName,
createdById: createdById ?? this.createdById, createdById: createdById ?? this.createdById,
@@ -279,8 +276,9 @@ class TicketModel extends Equatable {
: null, : null,
ticketResult: TicketResult.fromString(map['ticket_result'] as String?), ticketResult: TicketResult.fromString(map['ticket_result'] as String?),
resolutionNotes: map['resolution_notes'] as String?, resolutionNotes: map['resolution_notes'] as String?,
customerName: (map['customer']?['name'] as String?).myFormat(), customer: map['customer'] != null
customerEmail: (map['customer']?['email'] as String?).myFormat(), ? CustomerModel.fromMap(map['customer'] as Map<String, dynamic>)
: null,
targetModelName: (map['target_model']?['name_with_brand'] as String?) targetModelName: (map['target_model']?['name_with_brand'] as String?)
?.myFormat(), ?.myFormat(),
sourceModelName: (map['source_model']?['name_with_brand'] as String?) sourceModelName: (map['source_model']?['name_with_brand'] as String?)
@@ -354,8 +352,7 @@ class TicketModel extends Equatable {
ticketResult, ticketResult,
resolutionNotes, resolutionNotes,
includedAccessories, includedAccessories,
customerName, customer,
customerEmail,
targetModelName, targetModelName,
sourceModelName, sourceModelName,
createdById, createdById,

View File

@@ -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/models/ticket_status_extension.dart';
import 'package:flux/features/tickets/utils/ticket_pdf_service.dart'; import 'package:flux/features/tickets/utils/ticket_pdf_service.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:pdf/pdf.dart';
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
class TicketFormScreen extends StatefulWidget { class TicketFormScreen extends StatefulWidget {
@@ -32,7 +31,8 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _altPhoneCtrl = TextEditingController(); final _altPhoneCtrl = TextEditingController();
final _serialCtrl = TextEditingController(); final _targetSerialCtrl = TextEditingController();
final _sourceSerialCtrl = TextEditingController();
final _requestCtrl = TextEditingController(); final _requestCtrl = TextEditingController();
final _accessoriesCtrl = TextEditingController(); final _accessoriesCtrl = TextEditingController();
final _publicNotesCtrl = TextEditingController(); final _publicNotesCtrl = TextEditingController();
@@ -54,7 +54,8 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
@override @override
void dispose() { void dispose() {
_altPhoneCtrl.dispose(); _altPhoneCtrl.dispose();
_serialCtrl.dispose(); _targetSerialCtrl.dispose();
_sourceSerialCtrl.dispose();
_requestCtrl.dispose(); _requestCtrl.dispose();
_accessoriesCtrl.dispose(); _accessoriesCtrl.dispose();
_publicNotesCtrl.dispose(); _publicNotesCtrl.dispose();
@@ -68,7 +69,12 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
if (_altPhoneCtrl.text.isEmpty) { if (_altPhoneCtrl.text.isEmpty) {
_altPhoneCtrl.text = model.alternativePhoneNumber ?? ''; _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 (_requestCtrl.text.isEmpty) _requestCtrl.text = model.request;
if (_accessoriesCtrl.text.isEmpty) { if (_accessoriesCtrl.text.isEmpty) {
_accessoriesCtrl.text = model.includedAccessories ?? ''; _accessoriesCtrl.text = model.includedAccessories ?? '';
@@ -91,7 +97,8 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
void _flushControllersToCubit() { void _flushControllersToCubit() {
context.read<TicketFormCubit>().updateFields( context.read<TicketFormCubit>().updateFields(
alternativePhoneNumber: _altPhoneCtrl.text, alternativePhoneNumber: _altPhoneCtrl.text,
targetSn: _serialCtrl.text, targetSn: _targetSerialCtrl.text,
sourceSn: _sourceSerialCtrl.text,
request: _requestCtrl.text, request: _requestCtrl.text,
includedAccessories: _accessoriesCtrl.text, includedAccessories: _accessoriesCtrl.text,
publicNotes: _publicNotesCtrl.text, publicNotes: _publicNotesCtrl.text,
@@ -231,7 +238,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
_ActionButton( _ActionButton(
icon: Icons.email, icon: Icons.email,
label: "Invia Email", label: "Invia Email",
onTap: ticket.customerEmail != null ? () {} : null, onTap: ticket.customer!.email.isNotEmpty ? () {} : null,
), ),
_ActionButton( _ActionButton(
icon: Icons.close, icon: Icons.close,
@@ -362,6 +369,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
child: const Text('Ricevuta'), child: const Text('Ricevuta'),
), ),
), ),
const SizedBox(width: 12),
Expanded( Expanded(
flex: 1, flex: 1,
child: ElevatedButton( child: ElevatedButton(
@@ -401,17 +409,17 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(child: Column(children: [_cardAnagrafica(ticket)])),
child: Column(
children: [_cardAnagrafica(ticket), _cardDispositivo(ticket)],
),
),
const SizedBox(width: 24),
Expanded(child: Column(children: [_cardDettagli(ticket)])),
const SizedBox(width: 24), const SizedBox(width: 24),
Expanded( Expanded(
child: Column( child: Column(
children: [_cardCosti(ticket), _cardAssegnazione(ticket)], children: [_cardDettagli(ticket), _cardCosti(ticket)],
),
),
const SizedBox(width: 24),
Expanded(
child: Column(
children: [_cardDispositivi(ticket), _cardAssegnazione(ticket)],
), ),
), ),
], ],
@@ -425,7 +433,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
child: Column( child: Column(
children: [ children: [
_cardAnagrafica(ticket), _cardAnagrafica(ticket),
_cardDispositivo(ticket), _cardDispositivi(ticket),
_cardAssegnazione(ticket), _cardAssegnazione(ticket),
], ],
), ),
@@ -444,7 +452,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_cardAnagrafica(ticket), _cardAnagrafica(ticket),
_cardDispositivo(ticket), _cardDispositivi(ticket),
_cardDettagli(ticket), _cardDettagli(ticket),
_cardCosti(ticket), _cardCosti(ticket),
_cardAssegnazione(ticket), _cardAssegnazione(ticket),
@@ -471,8 +479,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
), ),
const Divider(height: 32), const Divider(height: 32),
SharedCustomerSection( SharedCustomerSection(
customerId: ticket.customerId, customer: ticket.customer,
customerName: ticket.customerName,
onCustomerSelected: (customer) => onCustomerSelected: (customer) =>
context.read<TicketFormCubit>().updateCustomer(customer), context.read<TicketFormCubit>().updateCustomer(customer),
), ),
@@ -488,14 +495,19 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
); );
} }
Widget _cardDispositivo(TicketModel ticket) { Widget _cardDispositivi(TicketModel ticket) {
final bool isDataTransfer = ticket.ticketType == TicketType.dataTransfer;
return _buildCard( return _buildCard(
title: 'Dispositivo', title: isDataTransfer ? 'Dispositivi' : 'Dispositivo',
icon: Icons.devices, icon: Icons.devices,
themeColor: Colors.deepOrange, themeColor: Colors.deepOrange,
children: [ children: [
// --- DISPOSITIVO TARGET (Nuovo/Ricevente) ---
SharedModelSection( SharedModelSection(
label: 'Modello da Riparare', label: isDataTransfer
? 'Dispositivo Target (Nuovo/Ricevente)'
: 'Modello da Riparare',
modelId: ticket.targetModelId, modelId: ticket.targetModelId,
modelName: ticket.targetModelName, modelName: ticket.targetModelName,
onModelSelected: (id, name) => context onModelSelected: (id, name) => context
@@ -504,12 +516,108 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: _serialCtrl, controller: _targetSerialCtrl, // Controller per il seriale TARGET
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Seriale / IMEI', labelText: 'Seriale / IMEI',
prefixIcon: Icon(Icons.qr_code), 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<TicketFormCubit>()
.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<TicketFormScreen> {
labelText: 'Tipo Lavorazione', labelText: 'Tipo Lavorazione',
), ),
items: TicketType.values items: TicketType.values
.map((t) => DropdownMenuItem(value: t, child: Text(t.name))) .map(
(t) => DropdownMenuItem(
value: t,
child: Text(t.displayValue),
),
)
.toList(), .toList(),
onChanged: (val) => context onChanged: (val) {
.read<TicketFormCubit>() context.read<TicketFormCubit>().updateFields(ticketType: val);
.updateFields(ticketType: val), },
), ),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
@@ -657,7 +770,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
const Divider(height: 32), const Divider(height: 32),
// ECCO LA MAGIA: // ECCO LA MAGIA:
SharedFilesSection( SharedFilesSection(
titleNameForUpload: ticket.customerName ?? 'Nuovo Ticket', titleNameForUpload: ticket.customer?.name ?? 'Nuovo Ticket',
onGenerateIdForQr: _generateIdForQr, onGenerateIdForQr: _generateIdForQr,
), ),
/* SharedAttachmentsSection( /* SharedAttachmentsSection(

View File

@@ -219,7 +219,7 @@ class _TicketCard extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
ticket.customerName ?? 'Cliente Sconosciuto', ticket.customer?.name ?? 'Cliente Sconosciuto',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,

View File

@@ -1,5 +1,3 @@
import 'dart:typed_data';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
@@ -152,7 +150,7 @@ class TicketPdfService {
pw.Expanded( pw.Expanded(
child: _infoBlock( child: _infoBlock(
"CLIENTE", "CLIENTE",
ticket.customerName ?? 'Cliente Sconosciuto', ticket.customer?.name ?? 'Cliente Sconosciuto',
font, font,
boldFont, boldFont,
), ),
@@ -317,7 +315,7 @@ class TicketPdfService {
style: pw.TextStyle(font: boldFont, fontSize: 10), style: pw.TextStyle(font: boldFont, fontSize: 10),
), ),
pw.Text( pw.Text(
ticket.customerName ?? 'Cliente sconosciuto', ticket.customer?.name ?? 'Cliente sconosciuto',
style: pw.TextStyle(font: font, fontSize: 9), style: pw.TextStyle(font: font, fontSize: 9),
), ),
pw.Text( pw.Text(

View File

@@ -309,6 +309,14 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: functions_client:
dependency: transitive dependency: transitive
description: description:
@@ -1043,7 +1051,7 @@ packages:
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
url_launcher: url_launcher:
dependency: transitive dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8

View File

@@ -33,7 +33,9 @@ dependencies:
uuid: ^4.5.3 uuid: ^4.5.3
pdf: ^3.12.0 pdf: ^3.12.0
universal_io: ^2.3.1 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: dev_dependencies:
flutter_test: flutter_test: