343 lines
10 KiB
Dart
343 lines
10 KiB
Dart
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();
|
|
}
|
|
}
|