Files
flux/lib/features/tickets/utils/ticket_pdf_service.dart

346 lines
10 KiB
Dart
Raw Normal View History

2026-05-11 20:44:17 +02:00
import 'dart:typed_data';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:get_it/get_it.dart';
2026-05-10 14:09:57 +02:00
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 {
final CompanyModel company = GetIt.I.get<SessionCubit>().state.company!;
2026-05-10 14:09:57 +02:00
/// Funzione principale: Genera il PDF A4 con le due metà
2026-05-11 20:44:17 +02:00
Future<pw.Document> generateTicketReceipt(TicketModel ticket) async {
2026-05-10 14:09:57 +02:00
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();
2026-05-11 18:19:48 +02:00
pw.Widget customerHalf = await _buildTicketHalf(
ticket,
company,
font,
boldFont,
isForCustomer: true,
);
pw.Widget storeHalf = await _buildTicketHalf(
ticket,
company,
font,
boldFont,
isForCustomer: false,
);
2026-05-10 14:09:57 +02:00
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat.a4,
margin: const pw.EdgeInsets.all(20),
2026-05-11 18:19:48 +02:00
build: (pw.Context context) {
2026-05-10 14:09:57 +02:00
return pw.Column(
children: [
// 1. METÀ SUPERIORE: CLIENTE
2026-05-11 18:19:48 +02:00
customerHalf,
2026-05-10 14:09:57 +02:00
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
2026-05-11 18:19:48 +02:00
storeHalf,
2026-05-10 14:09:57 +02:00
],
);
},
),
);
2026-05-11 20:44:17 +02:00
return pdf;
2026-05-10 14:09:57 +02:00
}
/// Helper per costruire una singola metà (Cliente o Negozio)
2026-05-11 18:19:48 +02:00
Future<pw.Widget> _buildTicketHalf(
2026-05-10 14:09:57 +02:00
TicketModel ticket,
CompanyModel company,
pw.Font font,
pw.Font boldFont, {
required bool isForCustomer,
2026-05-11 18:19:48 +02:00
}) async {
2026-05-11 11:44:14 +02:00
return pw.Expanded(
//height: 380, // Circa metà A4 meno i margini
2026-05-10 14:09:57 +02:00
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)),
],
);
}
2026-05-11 20:44:17 +02:00
Future<pw.Document> generateLabelPdf(TicketModel ticket) async {
2026-05-10 14:09:57 +02:00
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,
),
],
);
},
),
);
2026-05-11 11:44:14 +02:00
return pdf;
2026-05-10 14:09:57 +02:00
}
}