sistemati ticket
This commit is contained in:
@@ -72,9 +72,9 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
||||
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<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}) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -109,6 +120,7 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
||||
TicketStatus? status,
|
||||
String? request,
|
||||
String? targetSn,
|
||||
String? sourceSn,
|
||||
String? alternativePhoneNumber,
|
||||
bool? hasCourtesyDevice,
|
||||
String? includedAccessories,
|
||||
@@ -126,6 +138,7 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
||||
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:
|
||||
|
||||
@@ -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<String, dynamic>)
|
||||
: 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,
|
||||
|
||||
@@ -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<TicketFormScreen> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
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<TicketFormScreen> {
|
||||
@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<TicketFormScreen> {
|
||||
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<TicketFormScreen> {
|
||||
void _flushControllersToCubit() {
|
||||
context.read<TicketFormCubit>().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<TicketFormScreen> {
|
||||
_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<TicketFormScreen> {
|
||||
child: const Text('Ricevuta'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ElevatedButton(
|
||||
@@ -401,17 +409,17 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
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<TicketFormScreen> {
|
||||
child: Column(
|
||||
children: [
|
||||
_cardAnagrafica(ticket),
|
||||
_cardDispositivo(ticket),
|
||||
_cardDispositivi(ticket),
|
||||
_cardAssegnazione(ticket),
|
||||
],
|
||||
),
|
||||
@@ -444,7 +452,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_cardAnagrafica(ticket),
|
||||
_cardDispositivo(ticket),
|
||||
_cardDispositivi(ticket),
|
||||
_cardDettagli(ticket),
|
||||
_cardCosti(ticket),
|
||||
_cardAssegnazione(ticket),
|
||||
@@ -471,8 +479,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
),
|
||||
const Divider(height: 32),
|
||||
SharedCustomerSection(
|
||||
customerId: ticket.customerId,
|
||||
customerName: ticket.customerName,
|
||||
customer: ticket.customer,
|
||||
onCustomerSelected: (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(
|
||||
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<TicketFormScreen> {
|
||||
),
|
||||
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<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',
|
||||
),
|
||||
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<TicketFormCubit>()
|
||||
.updateFields(ticketType: val),
|
||||
onChanged: (val) {
|
||||
context.read<TicketFormCubit>().updateFields(ticketType: val);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
@@ -657,7 +770,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
const Divider(height: 32),
|
||||
// ECCO LA MAGIA:
|
||||
SharedFilesSection(
|
||||
titleNameForUpload: ticket.customerName ?? 'Nuovo Ticket',
|
||||
titleNameForUpload: ticket.customer?.name ?? 'Nuovo Ticket',
|
||||
onGenerateIdForQr: _generateIdForQr,
|
||||
),
|
||||
/* SharedAttachmentsSection(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user