// lib/services/report_pdf_service.dart import 'dart:io'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:shared_preferences/shared_preferences.dart'; import '../models/report_transaction.dart'; import '../models/report_signature_config.dart'; import '../utils/currency_format.dart'; import 'pdf_snapshot_service.dart'; class ReportPdfService { /// Membagi daftar transaksi menjadi halaman-halaman sehingga lembar terakhir /// dijamin memiliki minimal 5 baris transaksi bersama kotak tanda tangan. static List> paginateTransactions( List transactions, ) { if (transactions.isEmpty) return [[]]; // Jika transaksi muat dalam 1 halaman bersama tanda tangan (<= 28 baris) if (transactions.length <= 28) { return [transactions]; } // Kapasitas maksimum baris per halaman penuh (tanpa tanda tangan): ~34 baris // Minimal baris di lembar terakhir bersama tanda tangan: 5 baris const int maxRowsFullPage = 34; const int minRowsLastPage = 5; final pages = >[]; int currentIndex = 0; while (currentIndex < transactions.length) { final remaining = transactions.length - currentIndex; // Jika sisa transaksi bisa menjadi halaman terakhir (<= 22 baris dan >= 5 baris) if (pages.isNotEmpty && remaining <= 22 && remaining >= minRowsLastPage) { pages.add(transactions.sublist(currentIndex)); break; } // Jika mendekati halaman terakhir if (remaining <= maxRowsFullPage + minRowsLastPage) { if (remaining <= maxRowsFullPage) { // remaining antara 29 s/d 34: alokasikan minimal 5 baris untuk halaman terakhir! final rowsForCurrent = remaining - minRowsLastPage; pages.add(transactions.sublist(currentIndex, currentIndex + rowsForCurrent)); currentIndex += rowsForCurrent; pages.add(transactions.sublist(currentIndex)); break; } else { // remaining antara 35 s/d 39: pastikan halaman terakhir dapat minimal 5 baris int rowsForCurrent = maxRowsFullPage; if (remaining - rowsForCurrent < minRowsLastPage) { rowsForCurrent = remaining - minRowsLastPage; } pages.add(transactions.sublist(currentIndex, currentIndex + rowsForCurrent)); currentIndex += rowsForCurrent; pages.add(transactions.sublist(currentIndex)); break; } } else { // Masih banyak transaksi (> 39 baris), ambil 1 halaman penuh pages.add(transactions.sublist(currentIndex, currentIndex + maxRowsFullPage)); currentIndex += maxRowsFullPage; } } return pages; } /// Menghasilkan file PDF dokumen Laporan Keuangan format A4 static Future generateReportPdf({ required List transactions, required DateTime startDate, required DateTime endDate, required ReportSignatureConfig config, String? accountName, File? targetFile, String? fireflyBaseUrl, }) async { final pdf = pw.Document(); String? baseUrl = fireflyBaseUrl; if (baseUrl == null || baseUrl.isEmpty) { try { final prefs = await SharedPreferences.getInstance(); baseUrl = prefs.getString('firefly_url'); } catch (_) {} } final dateShortFmt = DateFormat('dd/MM/yyyy'); String formatIndonesianDate(DateTime d) { const months = [ '', 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember' ]; return '${d.day} ${months[d.month]} ${d.year}'; } final primaryGreen = PdfColor.fromHex('#14532D'); final lightGreen = PdfColor.fromHex('#F0FDF4'); final headerGrey = PdfColor.fromHex('#E5E7EB'); final borderGrey = PdfColor.fromHex('#9CA3AF'); // Perhitungan total double totalPemasukan = 0; double totalPengeluaran = 0; for (final t in transactions) { if (t.isPemasukan) { totalPemasukan += t.amount; } else { totalPengeluaran += t.amount; } } final finalSaldo = totalPemasukan - totalPengeluaran; // ==================== HALAMAN TABEL REKAPITULASI KAS ==================== final pagedTransactions = paginateTransactions(transactions); double currentRunningSaldo = 0; int globalRowOffset = 0; for (int pageIndex = 0; pageIndex < pagedTransactions.length; pageIndex++) { final pageTx = pagedTransactions[pageIndex]; final isLastSummaryPage = (pageIndex == pagedTransactions.length - 1); final startRunningSaldo = currentRunningSaldo; final startIndex = globalRowOffset; // Akumulasikan running saldo untuk halaman berikutnya for (final t in pageTx) { final pem = t.isPemasukan ? t.amount : 0.0; final peng = t.isPengeluaran ? t.amount : 0.0; currentRunningSaldo += (pem - peng); } globalRowOffset += pageTx.length; pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.symmetric(horizontal: 28, vertical: 22), build: (context) { return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.stretch, children: [ // 1. Header Kop Laporan pw.Center( child: pw.Text( config.reportTitle.toUpperCase(), style: pw.TextStyle( fontSize: 13, fontWeight: pw.FontWeight.bold, color: PdfColors.black, ), textAlign: pw.TextAlign.center, ), ), pw.SizedBox(height: 2), pw.Center( child: pw.Text( 'PERIODE ${dateShortFmt.format(startDate)} - ${dateShortFmt.format(endDate)}' '${accountName != null && accountName.isNotEmpty ? ' ($accountName)' : ''}', style: pw.TextStyle( fontSize: 9.5, fontWeight: pw.FontWeight.normal, color: PdfColors.grey800, ), textAlign: pw.TextAlign.center, ), ), pw.SizedBox(height: 8), // 2. Tabel Transaksi _buildTransactionTable( transactions: pageTx, startIndex: startIndex, dateShortFmt: dateShortFmt, primaryGreen: primaryGreen, headerGrey: headerGrey, borderGrey: borderGrey, isLastPage: isLastSummaryPage, totalPemasukan: totalPemasukan, totalPengeluaran: totalPengeluaran, finalSaldo: finalSaldo, initialRunningSaldo: startRunningSaldo, fireflyBaseUrl: baseUrl, ), // 3. Kotak Tanda Tangan (Hanya di lembar terakhir rekapitulasi) if (isLastSummaryPage) ...[ pw.SizedBox(height: 16), _buildSignatureBlock(config, endDate, formatIndonesianDate), ], ], ); }, ), ); } // ==================== HALAMAN LAMPIRAN BUKTI PENGELUARAN ==================== final pengeluaranWithAttachment = transactions .where((t) => t.isPengeluaran && (t.hasFotoBarang || t.hasInvoice)) .toList(); if (pengeluaranWithAttachment.isNotEmpty) { // Halaman Cover Lampiran pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(36), build: (context) { return pw.Center( child: pw.Column( mainAxisAlignment: pw.MainAxisAlignment.center, crossAxisAlignment: pw.CrossAxisAlignment.center, children: [ pw.Text( 'LAMPIRAN', style: pw.TextStyle( fontSize: 26, fontWeight: pw.FontWeight.bold, color: primaryGreen, ), ), pw.SizedBox(height: 10), pw.Text( 'BUKTI PENGELUARAN', style: pw.TextStyle( fontSize: 18, fontWeight: pw.FontWeight.bold, color: PdfColors.black, ), ), pw.SizedBox(height: 16), pw.Text( '${config.reportTitle} - Periode ${dateShortFmt.format(startDate)} - ${dateShortFmt.format(endDate)}', style: const pw.TextStyle(fontSize: 11, color: PdfColors.grey700), ), pw.SizedBox(height: 8), pw.Text( 'Jumlah Lampiran: ${pengeluaranWithAttachment.length} transaksi', style: pw.TextStyle( fontSize: 11, fontWeight: pw.FontWeight.bold, color: PdfColors.grey900, ), ), ], ), ); }, ), ); // Detail Lampiran Tiap Transaksi Pengeluaran for (int i = 0; i < pengeluaranWithAttachment.length; i++) { final t = pengeluaranWithAttachment[i]; pw.Widget? fotoBarangWidget; if (t.hasFotoBarang && File(t.fotoBarangPath!).existsSync()) { try { final bytes = File(t.fotoBarangPath!).readAsBytesSync(); final image = pw.MemoryImage(bytes); fotoBarangWidget = pw.Container( margin: const pw.EdgeInsets.only(top: 8, bottom: 14), constraints: const pw.BoxConstraints(maxHeight: 250), alignment: pw.Alignment.center, child: pw.Image(image, fit: pw.BoxFit.contain), ); } catch (e) { fotoBarangWidget = pw.Text('(Gagal memuat foto barang: $e)'); } } pw.Widget? invoiceWidget; if (t.hasInvoice && File(t.invoicePath!).existsSync()) { if (t.invoiceType == 'image') { try { final bytes = File(t.invoicePath!).readAsBytesSync(); final image = pw.MemoryImage(bytes); invoiceWidget = pw.Container( margin: const pw.EdgeInsets.only(top: 8, bottom: 14), constraints: const pw.BoxConstraints(maxHeight: 250), alignment: pw.Alignment.center, child: pw.Image(image, fit: pw.BoxFit.contain), ); } catch (e) { invoiceWidget = pw.Text('(Gagal memuat gambar invoice: $e)'); } } else { // PDF file String? snapshotPath = t.invoiceSnapshotPath; // Jika belum ada snapshot, coba render on-the-fly if (snapshotPath == null || !File(snapshotPath).existsSync()) { try { final pdfFile = File(t.invoicePath!); final targetPng = '${pdfFile.parent.path}/snapshot_${t.id}_${pdfFile.uri.pathSegments.last}.png'; snapshotPath = await PdfSnapshotService.renderFirstPage( pdfPath: t.invoicePath!, outputPngPath: targetPng, ); if (snapshotPath != null) { t.invoiceSnapshotPath = snapshotPath; } } catch (_) {} } if (snapshotPath != null && File(snapshotPath).existsSync()) { try { final bytes = File(snapshotPath).readAsBytesSync(); final image = pw.MemoryImage(bytes); invoiceWidget = pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Container( margin: const pw.EdgeInsets.only(top: 8, bottom: 8), constraints: const pw.BoxConstraints(maxHeight: 250), alignment: pw.Alignment.center, child: pw.Image(image, fit: pw.BoxFit.contain), ), pw.Container( padding: const pw.EdgeInsets.symmetric( horizontal: 8, vertical: 4), decoration: pw.BoxDecoration( color: headerGrey, borderRadius: const pw.BorderRadius.all(pw.Radius.circular(4)), border: pw.Border.all(color: borderGrey, width: 0.5), ), child: pw.Text( '📄 Cuplikan Berkas PDF: ${t.invoiceName ?? "invoice.pdf"}', style: pw.TextStyle( fontSize: 8.5, color: primaryGreen, fontWeight: pw.FontWeight.bold, ), ), ), ], ); } catch (e) { invoiceWidget = pw.Text('(Gagal memuat snapshot PDF: $e)'); } } else { // Fallback jika render snapshot tidak tersedia invoiceWidget = pw.Container( margin: const pw.EdgeInsets.only(top: 6, bottom: 10), padding: const pw.EdgeInsets.all(12), decoration: pw.BoxDecoration( color: headerGrey, borderRadius: const pw.BorderRadius.all(pw.Radius.circular(6)), border: pw.Border.all(color: borderGrey, width: 0.5), ), child: pw.Row( children: [ pw.Text( '📄 Dokumen Lampiran Digital: ${t.invoiceName ?? "invoice.pdf"}', style: pw.TextStyle( fontWeight: pw.FontWeight.bold, fontSize: 9.5, color: primaryGreen, ), ), ], ), ); } } } pdf.addPage( pw.Page( pageFormat: PdfPageFormat.a4, margin: const pw.EdgeInsets.all(32), build: (context) { return pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // Card info transaksi pw.Container( width: double.infinity, padding: const pw.EdgeInsets.all(10), decoration: pw.BoxDecoration( color: lightGreen, border: pw.Border( left: pw.BorderSide(color: primaryGreen, width: 4), top: pw.BorderSide(color: borderGrey, width: 0.5), right: pw.BorderSide(color: borderGrey, width: 0.5), bottom: pw.BorderSide(color: borderGrey, width: 0.5), ), ), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( 'Lampiran ${i + 1} dari ${pengeluaranWithAttachment.length}', style: pw.TextStyle( fontSize: 10, fontWeight: pw.FontWeight.bold, color: primaryGreen, ), ), pw.SizedBox(height: 4), pw.Text( 'Uraian: ${t.description}', style: pw.TextStyle(fontSize: 9, fontWeight: pw.FontWeight.bold), ), pw.SizedBox(height: 2), pw.Row( children: [ pw.Text( 'Tanggal: ${dateShortFmt.format(t.date)}', style: const pw.TextStyle(fontSize: 8.5), ), pw.SizedBox(width: 16), pw.Text( 'Pengeluaran: ${CurrencyFormat.formatRupiah(t.amount)}', style: pw.TextStyle( fontSize: 8.5, fontWeight: pw.FontWeight.bold, color: PdfColors.red800, ), ), pw.SizedBox(width: 16), pw.Text( 'Tujuan: ${t.counterpartyName}', style: const pw.TextStyle(fontSize: 8.5), ), ], ), ], ), ), pw.SizedBox(height: 14), // A. Foto Barang if (fotoBarangWidget != null) ...[ pw.Text( 'A. Foto Fisik Barang', style: pw.TextStyle( fontSize: 10, fontWeight: pw.FontWeight.bold, color: primaryGreen, ), ), fotoBarangWidget, ], // B. Invoice if (invoiceWidget != null) ...[ pw.Text( 'B. Invoice / Bukti Pembayaran ${t.invoiceName != null ? "(${t.invoiceName})" : ""}', style: pw.TextStyle( fontSize: 10, fontWeight: pw.FontWeight.bold, color: primaryGreen, ), ), invoiceWidget, ], ], ); }, ), ); } } final bytes = await pdf.save(); if (targetFile != null) { await targetFile.writeAsBytes(bytes); return targetFile; } // Simpan ke direktori dokumen lokal final outputDir = await getApplicationDocumentsDirectory(); final fileName = 'Laporan_Keuangan_${dateShortFmt.format(startDate).replaceAll('/', '-')}_sd_${dateShortFmt.format(endDate).replaceAll('/', '-')}.pdf'; final file = File('${outputDir.path}/$fileName'); await file.writeAsBytes(bytes); return file; } /// Membangun tabel transaksi dengan kolom yang presisi, link ke Firefly III, /// dan baris TOTAL yang 100% sejajar dengan kolom tabel. static pw.Table _buildTransactionTable({ required List transactions, required int startIndex, required DateFormat dateShortFmt, required PdfColor primaryGreen, required PdfColor headerGrey, required PdfColor borderGrey, required bool isLastPage, required double totalPemasukan, required double totalPengeluaran, required double finalSaldo, required double initialRunningSaldo, required String? fireflyBaseUrl, }) { final columnWidths = { 0: const pw.FixedColumnWidth(22), // No 1: const pw.FixedColumnWidth(55), // Tanggal 2: const pw.FlexColumnWidth(2.2), // Uraian 3: const pw.FixedColumnWidth(70), // Pemasukan 4: const pw.FixedColumnWidth(70), // Pengeluaran 5: const pw.FixedColumnWidth(72), // Saldo 6: const pw.FlexColumnWidth(1.4), // Keterangan }; final rows = []; // 1. Table Header Row rows.add( pw.TableRow( decoration: pw.BoxDecoration(color: primaryGreen), children: [ _headerCell('No', alignment: pw.Alignment.center), _headerCell('Tanggal', alignment: pw.Alignment.center), _headerCell('Uraian', alignment: pw.Alignment.centerLeft), _headerCell('Pemasukan', alignment: pw.Alignment.centerRight), _headerCell('Pengeluaran', alignment: pw.Alignment.centerRight), _headerCell('Saldo', alignment: pw.Alignment.centerRight), _headerCell('Keterangan', alignment: pw.Alignment.centerLeft), ], ), ); // 2. Data Rows double runningSaldo = initialRunningSaldo; for (int i = 0; i < transactions.length; i++) { final t = transactions[i]; final pemasukan = t.isPemasukan ? t.amount : 0.0; final pengeluaran = t.isPengeluaran ? t.amount : 0.0; runningSaldo += (pemasukan - pengeluaran); final rowNum = startIndex + i + 1; // Link ke Firefly III untuk kolom Keterangan final txId = (t.transactionGroupId != null && t.transactionGroupId!.isNotEmpty) ? t.transactionGroupId! : t.id; final txUrl = (fireflyBaseUrl != null && fireflyBaseUrl.isNotEmpty && txId.isNotEmpty) ? '${fireflyBaseUrl.replaceAll(RegExp(r'/+$'), '')}/transactions/show/$txId' : null; pw.Widget counterpartyWidget = pw.Text( t.counterpartyName, style: pw.TextStyle( fontSize: 8, color: txUrl != null ? PdfColors.blue800 : PdfColors.black, decoration: txUrl != null ? pw.TextDecoration.underline : pw.TextDecoration.none, ), ); if (txUrl != null) { counterpartyWidget = pw.UrlLink( destination: txUrl, child: counterpartyWidget, ); } rows.add( pw.TableRow( children: [ _dataCell('$rowNum', alignment: pw.Alignment.center), _dataCell(dateShortFmt.format(t.date), alignment: pw.Alignment.center), _dataCell(t.description, alignment: pw.Alignment.centerLeft), _dataCell(pemasukan > 0 ? CurrencyFormat.formatRupiah(pemasukan) : '-', alignment: pw.Alignment.centerRight), _dataCell(pengeluaran > 0 ? CurrencyFormat.formatRupiah(pengeluaran) : '-', alignment: pw.Alignment.centerRight), _dataCell(CurrencyFormat.formatRupiah(runningSaldo), alignment: pw.Alignment.centerRight), _dataCellWidget(counterpartyWidget, alignment: pw.Alignment.centerLeft), ], ), ); } // 3. Baris TOTAL (Hanya pada lembar terakhir rekapitulasi transaksi) // Ditempatkan langsung sebagai TableRow agar 100% presisi dan lurus dengan kolom di atasnya! if (isLastPage) { rows.add( pw.TableRow( decoration: pw.BoxDecoration(color: headerGrey), children: [ _totalCell(''), // No _totalCell(''), // Tanggal _totalCell('TOTAL', alignment: pw.Alignment.centerRight, isBold: true, fontSize: 8.5), _totalCell(CurrencyFormat.formatRupiah(totalPemasukan), alignment: pw.Alignment.centerRight, isBold: true), _totalCell(CurrencyFormat.formatRupiah(totalPengeluaran), alignment: pw.Alignment.centerRight, isBold: true), _totalCell(CurrencyFormat.formatRupiah(finalSaldo), alignment: pw.Alignment.centerRight, isBold: true), _totalCell(''), // Keterangan ], ), ); } return pw.Table( border: pw.TableBorder.all(color: borderGrey, width: 0.5), columnWidths: columnWidths, children: rows, ); } static pw.Widget _headerCell(String text, {required pw.Alignment alignment}) { return pw.Container( alignment: alignment, padding: const pw.EdgeInsets.symmetric(horizontal: 4, vertical: 4), child: pw.Text( text, style: pw.TextStyle( color: PdfColors.white, fontWeight: pw.FontWeight.bold, fontSize: 8.5, ), ), ); } static pw.Widget _dataCell(String text, {required pw.Alignment alignment}) { return pw.Container( alignment: alignment, padding: const pw.EdgeInsets.symmetric(horizontal: 4, vertical: 3.5), child: pw.Text( text, style: const pw.TextStyle(fontSize: 8), ), ); } static pw.Widget _dataCellWidget(pw.Widget child, {required pw.Alignment alignment}) { return pw.Container( alignment: alignment, padding: const pw.EdgeInsets.symmetric(horizontal: 4, vertical: 3.5), child: child, ); } static pw.Widget _totalCell( String text, { pw.Alignment alignment = pw.Alignment.center, bool isBold = false, double fontSize = 8, }) { return pw.Container( alignment: alignment, padding: const pw.EdgeInsets.symmetric(horizontal: 4, vertical: 4), child: pw.Text( text, style: pw.TextStyle( fontSize: fontSize, fontWeight: isBold ? pw.FontWeight.bold : pw.FontWeight.normal, color: PdfColors.black, ), ), ); } /// Membangun blok tanda tangan 4 pihak dalam formasi grid 2x2 static pw.Widget _buildSignatureBlock( ReportSignatureConfig config, DateTime endDate, String Function(DateTime) formatIndonesianDate, ) { return pw.Container( margin: const pw.EdgeInsets.only(top: 6), child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ // Baris 1: Pelaksana UPJ (Kiri) & Pembuat Laporan (Kanan) pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ // Kiri Atas: Pelaksana UPJ Mekatronika (Mengetahui) pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( config.signatory1Role, style: const pw.TextStyle(fontSize: 8.5), ), pw.SizedBox(height: 38), pw.Text( config.signatory1Name, style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold, decoration: pw.TextDecoration.underline, ), ), if (config.signatory1Nip.isNotEmpty) pw.Text( config.signatory1Nip.startsWith('N') ? config.signatory1Nip : 'NIP. ${config.signatory1Nip}', style: const pw.TextStyle(fontSize: 8), ), ], ), ), // Kanan Atas: Dibuat oleh (dengan Kota & Tanggal) pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ pw.Container( width: 200, child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( '${config.city}, ${formatIndonesianDate(endDate)}', style: const pw.TextStyle(fontSize: 8.5), ), pw.SizedBox(height: 2), pw.Text( config.signatory2Role, style: const pw.TextStyle(fontSize: 8.5), ), pw.SizedBox(height: 38), pw.Text( config.signatory2Name, style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold, decoration: pw.TextDecoration.underline, ), ), if (config.signatory2Nip.isNotEmpty) pw.Text( config.signatory2Nip.startsWith('N') ? config.signatory2Nip : 'NIP. ${config.signatory2Nip}', style: const pw.TextStyle(fontSize: 8), ), ], ), ), ], ), ), ], ), pw.SizedBox(height: 18), // Baris 2: Koordinator UPJ (Kiri) & KPA BLUD SMKN 4 Malang (Kanan) pw.Row( crossAxisAlignment: pw.CrossAxisAlignment.start, mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, children: [ // Kiri Bawah: Koordinator UPJ pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( config.signatory3Role, style: const pw.TextStyle(fontSize: 8.5), ), pw.SizedBox(height: 38), pw.Text( config.signatory3Name, style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold, decoration: pw.TextDecoration.underline, ), ), if (config.signatory3Nip.isNotEmpty) pw.Text( config.signatory3Nip.startsWith('N') ? config.signatory3Nip : 'NIP. ${config.signatory3Nip}', style: const pw.TextStyle(fontSize: 8), ), ], ), ), // Kanan Bawah: KPA BLUD SMKN 4 Malang pw.Expanded( child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.end, children: [ pw.Container( width: 200, child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ pw.Text( config.signatory4Role, style: const pw.TextStyle(fontSize: 8.5), ), pw.SizedBox(height: 38), pw.Text( config.signatory4Name, style: pw.TextStyle( fontSize: 9, fontWeight: pw.FontWeight.bold, decoration: pw.TextDecoration.underline, ), ), if (config.signatory4Nip.isNotEmpty) pw.Text( config.signatory4Nip.startsWith('N') ? config.signatory4Nip : 'NIP. ${config.signatory4Nip}', style: const pw.TextStyle(fontSize: 8), ), ], ), ), ], ), ), ], ), ], ), ); } }