ticket labels e ticket receipt
This commit is contained in:
342
lib/features/tickets/utils/ticket_pdf_service.dart
Normal file
342
lib/features/tickets/utils/ticket_pdf_service.dart
Normal file
@@ -0,0 +1,342 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||
import 'package:flux/features/company/models/company_model.dart';
|
||||
|
||||
class TicketPdfService {
|
||||
/// Funzione principale: Genera il PDF A4 con le due metà
|
||||
Future<Uint8List> generateTicketReceipt(
|
||||
TicketModel ticket,
|
||||
CompanyModel company,
|
||||
) async {
|
||||
final pdf = pw.Document();
|
||||
|
||||
// Carichiamo il font per essere sicuri che i caratteri siano ok
|
||||
final font = await PdfGoogleFonts.robotoRegular();
|
||||
final boldFont = await PdfGoogleFonts.robotoBold();
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
margin: const pw.EdgeInsets.all(20),
|
||||
build: (context) {
|
||||
return pw.Column(
|
||||
children: [
|
||||
// 1. METÀ SUPERIORE: CLIENTE
|
||||
_buildTicketHalf(
|
||||
ticket,
|
||||
company,
|
||||
font,
|
||||
boldFont,
|
||||
isForCustomer: true,
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
// Linea tratteggiata per il taglio
|
||||
pw.Container(
|
||||
margin: const pw.EdgeInsets.symmetric(vertical: 10),
|
||||
child: pw.Text(
|
||||
'-' * 100,
|
||||
style: const pw.TextStyle(color: PdfColors.grey400),
|
||||
),
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
// 2. METÀ INFERIORE: NEGOZIO
|
||||
_buildTicketHalf(
|
||||
ticket,
|
||||
company,
|
||||
font,
|
||||
boldFont,
|
||||
isForCustomer: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return pdf.save();
|
||||
}
|
||||
|
||||
/// Helper per costruire una singola metà (Cliente o Negozio)
|
||||
pw.Widget _buildTicketHalf(
|
||||
TicketModel ticket,
|
||||
CompanyModel company,
|
||||
pw.Font font,
|
||||
pw.Font boldFont, {
|
||||
required bool isForCustomer,
|
||||
}) {
|
||||
return pw.Container(
|
||||
height: 380, // Circa metà A4 meno i margini
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
// HEADER: Logo e Dati Azienda (Solo per cliente o ID per negozio)
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
company.name,
|
||||
style: pw.TextStyle(font: boldFont, fontSize: 16),
|
||||
),
|
||||
if (isForCustomer) ...[
|
||||
pw.Text(
|
||||
"${company.address}, ${company.city}",
|
||||
style: const pw.TextStyle(fontSize: 10),
|
||||
),
|
||||
pw.Text(
|
||||
"P.IVA: ${company.vatId}",
|
||||
style: const pw.TextStyle(fontSize: 10),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||
children: [
|
||||
pw.Text(
|
||||
isForCustomer
|
||||
? "RICEVUTA CLIENTE"
|
||||
: "COPIA INTERNA NEGOZIO",
|
||||
style: pw.TextStyle(
|
||||
font: boldFont,
|
||||
fontSize: 12,
|
||||
color: PdfColors.grey700,
|
||||
),
|
||||
),
|
||||
pw.Text(
|
||||
"Rif: ${ticket.referenceId}",
|
||||
style: pw.TextStyle(font: boldFont, fontSize: 14),
|
||||
),
|
||||
pw.Text(
|
||||
"Data: ${ticket.createdAt?.toString().substring(0, 10) ?? ''}",
|
||||
style: const pw.TextStyle(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(width: 10),
|
||||
// IL NOSTRO QR CODE MAGICO
|
||||
pw.BarcodeWidget(
|
||||
barcode: pw.Barcode.qrCode(),
|
||||
data: ticket.id!, // Salviamo l'ID univoco nel QR!
|
||||
width: 45,
|
||||
height: 45,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
pw.Divider(thickness: 1),
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
// DATI CLIENTE
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: _infoBlock(
|
||||
"CLIENTE",
|
||||
ticket.customerName ?? 'Cliente Sconosciuto',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
),
|
||||
pw.Expanded(
|
||||
child: _infoBlock(
|
||||
"CONTATTO ALTERNATIVO",
|
||||
ticket.alternativePhoneNumber ?? 'N/D',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 15),
|
||||
|
||||
// DETTAGLI LAVORAZIONE
|
||||
_infoBlock(
|
||||
"DESCRIZIONE PROBLEMA / LAVORAZIONE RICHIESTA",
|
||||
ticket.request,
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: _infoBlock(
|
||||
"ACCESSORI CONSEGNATI",
|
||||
ticket.includedAccessories ?? 'Nessuno',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
),
|
||||
pw.Expanded(
|
||||
child: _infoBlock(
|
||||
"GARANZIA",
|
||||
ticket.warrantyType?.displayValue ?? 'Standard',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 15),
|
||||
|
||||
// NOTE (Pubbliche o Private a seconda della copia)
|
||||
if (isForCustomer)
|
||||
_infoBlock("NOTE", ticket.publicNotes ?? '-', font, boldFont)
|
||||
else
|
||||
_infoBlock(
|
||||
"NOTE INTERNE (PRIVATE)",
|
||||
ticket.internalNotes ?? '-',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
|
||||
pw.Spacer(),
|
||||
|
||||
// FOOTER: Disclaimer e Firma
|
||||
if (!isForCustomer) ...[
|
||||
pw.Text(
|
||||
"CONDIZIONI E LIBERATORIA:",
|
||||
style: pw.TextStyle(font: boldFont, fontSize: 8),
|
||||
),
|
||||
pw.Text(
|
||||
company.ticketDisclaimer ??
|
||||
'Firma per accettazione delle condizioni di riparazione.',
|
||||
style: const pw.TextStyle(fontSize: 7),
|
||||
textAlign: pw.TextAlign.justify,
|
||||
),
|
||||
pw.SizedBox(height: 20),
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Container(
|
||||
width: 150,
|
||||
decoration: pw.BoxDecoration(
|
||||
border: const pw.Border(top: pw.BorderSide(width: 0.5)),
|
||||
),
|
||||
),
|
||||
pw.Text(
|
||||
"Firma del Cliente per accettazione",
|
||||
style: const pw.TextStyle(fontSize: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
] else
|
||||
pw.Align(
|
||||
alignment: pw.Alignment.centerRight,
|
||||
child: pw.Text(
|
||||
"Grazie per averci scelto!",
|
||||
style: pw.TextStyle(
|
||||
font: font,
|
||||
fontSize: 10,
|
||||
fontStyle: pw.FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pw.Widget _infoBlock(
|
||||
String label,
|
||||
String value,
|
||||
pw.Font font,
|
||||
pw.Font boldFont,
|
||||
) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
label,
|
||||
style: pw.TextStyle(
|
||||
font: boldFont,
|
||||
fontSize: 8,
|
||||
color: PdfColors.grey600,
|
||||
),
|
||||
),
|
||||
pw.Text(value, style: pw.TextStyle(font: font, fontSize: 11)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List> generateLabelPdf(
|
||||
TicketModel ticket,
|
||||
CompanyModel company,
|
||||
) async {
|
||||
final pdf = pw.Document();
|
||||
final font = await PdfGoogleFonts.robotoRegular();
|
||||
final boldFont = await PdfGoogleFonts.robotoBold();
|
||||
|
||||
// Prendiamo le misure salvate (se custom) o usiamo default
|
||||
final widthMm = company.labelWidth ?? 62.0;
|
||||
final heightMm = company.labelHeight ?? 29.0;
|
||||
|
||||
// Creiamo il formato fisico esatto!
|
||||
final format = company.isLabelVertical
|
||||
? PdfPageFormat(heightMm * PdfPageFormat.mm, widthMm * PdfPageFormat.mm)
|
||||
: PdfPageFormat(
|
||||
widthMm * PdfPageFormat.mm,
|
||||
heightMm * PdfPageFormat.mm,
|
||||
);
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
pageFormat: format,
|
||||
margin: const pw.EdgeInsets.all(2), // Margini minimi per le etichette
|
||||
build: (context) {
|
||||
return pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
children: [
|
||||
pw.Text(
|
||||
ticket.referenceId ?? '',
|
||||
style: pw.TextStyle(font: boldFont, fontSize: 10),
|
||||
),
|
||||
pw.Text(
|
||||
ticket.customerName ?? 'Cliente sconosciuto',
|
||||
style: pw.TextStyle(font: font, fontSize: 9),
|
||||
),
|
||||
pw.Text(
|
||||
ticket.createdAt?.toString().substring(0, 10) ?? '',
|
||||
style: const pw.TextStyle(fontSize: 7),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// QR Code compatto
|
||||
pw.BarcodeWidget(
|
||||
barcode: pw.Barcode.qrCode(),
|
||||
data: ticket.id!,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return pdf.save();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user