boh
This commit is contained in:
@@ -190,7 +190,7 @@ class HomeScreen extends StatelessWidget {
|
||||
// Entriamo nel form! Nessun parametro extra = Nuovo Servizio
|
||||
context.pushNamed(
|
||||
Routes.operationForm,
|
||||
pathParameters: {'id': 'New'},
|
||||
pathParameters: {'id': 'new'},
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -203,7 +203,7 @@ class HomeScreen extends StatelessWidget {
|
||||
// Andiamo alla lista! (Da lì poi aggiungeremo il tasto "+" per il form)
|
||||
context.pushNamed(
|
||||
Routes.ticketForm,
|
||||
pathParameters: {'id': 'New'},
|
||||
pathParameters: {'id': 'new'},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -193,6 +193,6 @@ class _OperationListScreenState extends State<OperationListScreen> {
|
||||
}
|
||||
|
||||
void startNewOperation(BuildContext context) {
|
||||
context.pushNamed('operation-form');
|
||||
context.pushNamed('operation-form', pathParameters: {'id': 'new'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,14 +73,15 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
||||
ticket: state.ticket.copyWith(
|
||||
customerId: customer.id,
|
||||
customerName: customer.name,
|
||||
alternativePhoneNumber: customer.phoneNumber, // Comodo come fallback!
|
||||
alternativePhoneNumber: customer.phoneNumber,
|
||||
customerEmail: customer.email,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 3. AGGIORNAMENTO MODELLO (Usato dal nostro SharedModelSection!)
|
||||
void updateModel({required String modelId, required String modelName}) {
|
||||
void updateTargetModel({required String modelId, required String modelName}) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
ticket: state.ticket.copyWith(
|
||||
@@ -143,7 +144,7 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
||||
}
|
||||
|
||||
/// 5. SALVATAGGIO
|
||||
Future<void> saveTicket({required bool keepAdding}) async {
|
||||
Future<void> saveTicket() async {
|
||||
emit(state.copyWith(status: TicketFormStatus.saving));
|
||||
|
||||
try {
|
||||
@@ -159,28 +160,15 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
||||
} else {
|
||||
savedTicket = await _repository.updateTicket(ticketToSave);
|
||||
}
|
||||
|
||||
if (keepAdding) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: TicketFormStatus.successAndAddAnother,
|
||||
// Svuotiamo il form per il prossimo, mantenendo Store e Creatore ATTUALI
|
||||
ticket: TicketModel.empty().copyWith(
|
||||
companyId: savedTicket.companyId,
|
||||
storeId: savedTicket.storeId,
|
||||
createdById: ticketToSave
|
||||
.createdById, // Manteniamo quello selezionato nella tendina!
|
||||
createdByName: ticketToSave.createdByName,
|
||||
ticketStatus: TicketStatus.open,
|
||||
ticketType: TicketType.repair,
|
||||
),
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: TicketFormStatus.success,
|
||||
ticket: ticketToSave.copyWith(
|
||||
id: savedTicket.id,
|
||||
referenceId: savedTicket.referenceId,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(status: TicketFormStatus.success, ticket: savedTicket),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
|
||||
@@ -2,15 +2,7 @@ import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||
// Adatta gli import al tuo progetto!
|
||||
|
||||
enum TicketFormStatus {
|
||||
initial,
|
||||
ready,
|
||||
loading,
|
||||
saving,
|
||||
success,
|
||||
successAndAddAnother,
|
||||
failure,
|
||||
}
|
||||
enum TicketFormStatus { initial, ready, loading, saving, success, pop, failure }
|
||||
|
||||
class TicketFormState extends Equatable {
|
||||
final TicketModel ticket;
|
||||
|
||||
@@ -107,6 +107,7 @@ class TicketModel extends Equatable {
|
||||
final TicketResult? ticketResult;
|
||||
final String? resolutionNotes;
|
||||
final String? customerName;
|
||||
final String? customerEmail;
|
||||
final String? targetModelName;
|
||||
final String? sourceModelName;
|
||||
final String? createdById;
|
||||
@@ -142,6 +143,7 @@ class TicketModel extends Equatable {
|
||||
this.ticketResult,
|
||||
this.resolutionNotes,
|
||||
this.customerName,
|
||||
this.customerEmail,
|
||||
this.targetModelName,
|
||||
this.sourceModelName,
|
||||
this.createdById,
|
||||
@@ -192,6 +194,7 @@ class TicketModel extends Equatable {
|
||||
TicketResult? ticketResult,
|
||||
String? resolutionNotes,
|
||||
String? customerName,
|
||||
String? customerEmail,
|
||||
String? targetModelName,
|
||||
String? sourceModelName,
|
||||
String? createdById,
|
||||
@@ -228,6 +231,7 @@ class TicketModel extends Equatable {
|
||||
ticketResult: ticketResult ?? this.ticketResult,
|
||||
resolutionNotes: resolutionNotes ?? this.resolutionNotes,
|
||||
customerName: customerName ?? this.customerName,
|
||||
customerEmail: customerEmail ?? this.customerEmail,
|
||||
targetModelName: targetModelName ?? this.targetModelName,
|
||||
sourceModelName: sourceModelName ?? this.sourceModelName,
|
||||
createdById: createdById ?? this.createdById,
|
||||
@@ -276,6 +280,7 @@ class TicketModel extends Equatable {
|
||||
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(),
|
||||
targetModelName: (map['target_model']?['name_with_brand'] as String?)
|
||||
?.myFormat(),
|
||||
sourceModelName: (map['source_model']?['name_with_brand'] as String?)
|
||||
@@ -350,6 +355,7 @@ class TicketModel extends Equatable {
|
||||
resolutionNotes,
|
||||
includedAccessories,
|
||||
customerName,
|
||||
customerEmail,
|
||||
targetModelName,
|
||||
sourceModelName,
|
||||
createdById,
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
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/shared_forms/customer_section.dart';
|
||||
import 'package:flux/core/widgets/shared_forms/model_section.dart';
|
||||
import 'package:flux/core/widgets/shared_forms/shared_files_section.dart';
|
||||
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||
import 'package:flux/features/company/models/company_model.dart';
|
||||
import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart';
|
||||
import 'package:flux/features/tickets/blocs/ticket_form_state.dart';
|
||||
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||
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 {
|
||||
final TicketModel? existingTicket;
|
||||
@@ -93,10 +99,10 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _saveTicket({required bool keepAdding}) {
|
||||
void _saveTicket() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_flushControllersToCubit();
|
||||
context.read<TicketFormCubit>().saveTicket(keepAdding: keepAdding);
|
||||
context.read<TicketFormCubit>().saveTicket();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +127,110 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
return newId;
|
||||
}
|
||||
|
||||
void _showSuccessActions(
|
||||
BuildContext context,
|
||||
TicketModel ticket,
|
||||
CompanyModel company,
|
||||
) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isDismissible: false, // Costringiamo l'operatore a una scelta conscia
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (context) => SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Icon(Icons.check_circle, color: Colors.green, size: 64),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Ticket Salvato!",
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"Rif: ${ticket.referenceId}",
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 18),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Griglia delle Azioni
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
childAspectRatio: 1.5,
|
||||
children: [
|
||||
_ActionButton(
|
||||
icon: Icons.print,
|
||||
label: "Ricevuta A4",
|
||||
onTap: () async {
|
||||
// 1. Costruiamo la struttura (velocissimo)
|
||||
final doc = await TicketPdfService()
|
||||
.generateTicketReceipt(ticket, company);
|
||||
|
||||
// 2. Lanciamo layoutPdf esattamente come facevi tu!
|
||||
await Printing.layoutPdf(
|
||||
name: 'Ricevuta_${ticket.referenceId}.pdf',
|
||||
onLayout: (PdfPageFormat format) async =>
|
||||
doc.save(), // La magia è qui!
|
||||
);
|
||||
},
|
||||
),
|
||||
if (company.labelFormat != LabelFormat.none)
|
||||
_ActionButton(
|
||||
icon: Icons.label,
|
||||
label: "Etichetta",
|
||||
onTap: () async {
|
||||
final doc = await TicketPdfService().generateLabelPdf(
|
||||
ticket,
|
||||
company,
|
||||
);
|
||||
await Printing.layoutPdf(
|
||||
name: 'Etichetta_${ticket.referenceId}.pdf',
|
||||
onLayout: (PdfPageFormat format) async => doc.save(),
|
||||
);
|
||||
},
|
||||
),
|
||||
_ActionButton(
|
||||
icon: Icons.email,
|
||||
label: "Invia Email",
|
||||
onTap: ticket.customerEmail != null ? () {} : null,
|
||||
),
|
||||
_ActionButton(
|
||||
icon: Icons.close,
|
||||
label: "Chiudi",
|
||||
color: Colors.blue[900],
|
||||
textColor: Colors.white,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -133,22 +243,13 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
}
|
||||
|
||||
if (state.status == TicketFormStatus.success) {
|
||||
Navigator.of(context).pop();
|
||||
} else if (state.status == TicketFormStatus.successAndAddAnother) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Scheda salvata! Inserisci la prossima.'),
|
||||
),
|
||||
_showSuccessActions(
|
||||
context,
|
||||
state.ticket,
|
||||
GetIt.I.get<SessionCubit>().state.company!,
|
||||
);
|
||||
_altPhoneCtrl.clear();
|
||||
_serialCtrl.clear();
|
||||
_requestCtrl.clear();
|
||||
_accessoriesCtrl.clear();
|
||||
_publicNotesCtrl.clear();
|
||||
_internalNotesCtrl.clear();
|
||||
_priceCtrl.clear();
|
||||
_costCtrl.clear();
|
||||
_isInitialized = false;
|
||||
} else if (state.status == TicketFormStatus.pop) {
|
||||
Navigator.of(context).pop();
|
||||
} else if (state.status == TicketFormStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -229,23 +330,23 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: OutlinedButton(
|
||||
onPressed: state.status == TicketFormStatus.saving
|
||||
child: ElevatedButton(
|
||||
onPressed: state.ticket.id == null
|
||||
? null
|
||||
: () => _saveTicket(keepAdding: true),
|
||||
child: const Text(
|
||||
'Salva e Aggiungi Altro',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
: () => _showSuccessActions(
|
||||
context,
|
||||
ticket,
|
||||
GetIt.I.get<SessionCubit>().state.company!,
|
||||
),
|
||||
child: const Text('Ricevuta'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: ElevatedButton(
|
||||
onPressed: state.status == TicketFormStatus.saving
|
||||
? null
|
||||
: () => _saveTicket(keepAdding: false),
|
||||
: () => _saveTicket(),
|
||||
child: state.status == TicketFormStatus.saving
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
@@ -378,7 +479,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
modelName: ticket.targetModelName,
|
||||
onModelSelected: (id, name) => context
|
||||
.read<TicketFormCubit>()
|
||||
.updateModel(modelId: id, modelName: name),
|
||||
.updateTargetModel(modelId: id, modelName: name),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
@@ -597,3 +698,55 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Widget helper per i bottoni dell'Action Hub
|
||||
class _ActionButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final VoidCallback? onTap;
|
||||
final Color? color;
|
||||
final Color? textColor;
|
||||
|
||||
const _ActionButton({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
this.onTap,
|
||||
this.color,
|
||||
this.textColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: onTap == null ? Colors.grey[100] : (color ?? Colors.grey[200]),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: onTap == null
|
||||
? Colors.grey
|
||||
: (textColor ?? Colors.blue[900]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: onTap == null
|
||||
? Colors.grey
|
||||
: (textColor ?? Colors.blue[900]),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ class _TicketListScreenState extends State<TicketListScreen> {
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
context.pushNamed(Routes.ticketForm, pathParameters: {'id': 'New'});
|
||||
context.pushNamed(Routes.ticketForm, pathParameters: {'id': 'new'});
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Nuovo Ticket'),
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:printing/printing.dart';
|
||||
@@ -7,7 +6,7 @@ import 'package:flux/features/company/models/company_model.dart';
|
||||
|
||||
class TicketPdfService {
|
||||
/// Funzione principale: Genera il PDF A4 con le due metà
|
||||
Future<Uint8List> generateTicketReceipt(
|
||||
Future<pw.Document> generateTicketReceipt(
|
||||
TicketModel ticket,
|
||||
CompanyModel company,
|
||||
) async {
|
||||
@@ -16,6 +15,8 @@ class TicketPdfService {
|
||||
// Carichiamo il font per essere sicuri che i caratteri siano ok
|
||||
final font = await PdfGoogleFonts.robotoRegular();
|
||||
final boldFont = await PdfGoogleFonts.robotoBold();
|
||||
/* final font = pw.Font.helvetica();
|
||||
final boldFont = pw.Font.helveticaBold(); */
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
@@ -60,7 +61,7 @@ class TicketPdfService {
|
||||
),
|
||||
);
|
||||
|
||||
return pdf.save();
|
||||
return pdf;
|
||||
}
|
||||
|
||||
/// Helper per costruire una singola metà (Cliente o Negozio)
|
||||
@@ -71,8 +72,8 @@ class TicketPdfService {
|
||||
pw.Font boldFont, {
|
||||
required bool isForCustomer,
|
||||
}) {
|
||||
return pw.Container(
|
||||
height: 380, // Circa metà A4 meno i margini
|
||||
return pw.Expanded(
|
||||
//height: 380, // Circa metà A4 meno i margini
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -276,7 +277,7 @@ class TicketPdfService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List> generateLabelPdf(
|
||||
Future<pw.Document> generateLabelPdf(
|
||||
TicketModel ticket,
|
||||
CompanyModel company,
|
||||
) async {
|
||||
@@ -337,6 +338,6 @@ class TicketPdfService {
|
||||
),
|
||||
);
|
||||
|
||||
return pdf.save();
|
||||
return pdf;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user