From 860a929da80e3f79b0c62412f3264b19b4d283bf Mon Sep 17 00:00:00 2001 From: a2nr Date: Sun, 28 Jun 2026 21:55:17 +0700 Subject: [PATCH] fix: perbaiki item dan total pecah baris di struk thermal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Library flutter_esc_pos_utils mengirim ESC a (align) di tengah baris pada generator.row(), menyebabkan kolom right-aligned tercetak di baris baru. Ganti semua row() campuran left+right alignment dengan text() + _padRow() (manual space padding 32 char) agar tidak ada perintah ESC a di tengah baris. - Tambah helper _padRow(left, right) untuk padding 32 karakter - Item: deskripsi 1 baris (width 12), qtyƗprice + total 1 baris - Subtotal, TOTAL, BAYAR, KEMBALI/TIP, KURANG: text() + _padRow() --- lib/screens/receipt_screen.dart | 14 +- lib/services/esc_pos_print_service.dart | 171 +++++++----------------- 2 files changed, 55 insertions(+), 130 deletions(-) diff --git a/lib/screens/receipt_screen.dart b/lib/screens/receipt_screen.dart index 8a62978..078a954 100644 --- a/lib/screens/receipt_screen.dart +++ b/lib/screens/receipt_screen.dart @@ -460,15 +460,11 @@ class _ReceiptScreenState extends State { ), ), if (_isPrinting) - Positioned( - top: 50, - right: 16, - child: PrintingStatusCard( - isVisible: _isPrinting, - onDismiss: () { - setState(() => _isPrinting = false); - }, - ), + PrintingStatusCard( + isVisible: _isPrinting, + onDismiss: () { + setState(() => _isPrinting = false); + }, ), ], ), diff --git a/lib/services/esc_pos_print_service.dart b/lib/services/esc_pos_print_service.dart index 14c5d8b..16e23fe 100644 --- a/lib/services/esc_pos_print_service.dart +++ b/lib/services/esc_pos_print_service.dart @@ -122,6 +122,9 @@ class EscPosPrintService { // Generate all receipt sections List bytes = []; + bytes.addAll(generator.reset()); + bytes.addAll([0x1D, 0x21, 0x00]); // GS ! 0 = paksa ukuran karakter normal + // Add store logo if available (diproses di isolate terpisah) bytes.addAll(await _addStoreLogo(generator, storeConfig.logoPath)); @@ -137,26 +140,17 @@ class EscPosPrintService { // Header grup: satu garis + nama sumber (tanpa "Dari:") bytes.addAll(generator.text('--------------------------------', styles: PosStyles(align: PosAlign.center))); - bytes.addAll(generator.text( - group.sourceAccountName ?? '(belum dipilih)', - styles: PosStyles(align: PosAlign.left, bold: true))); + bytes.addAll(generator.text(group.sourceAccountName ?? '(belum dipilih)', + styles: PosStyles(align: PosAlign.center, bold: true))); // Item list grup bytes.addAll(_addItemList(generator, group.items)); // Subtotal grup - bytes.addAll(generator.row([ - PosColumn( - text: 'Subtotal', - width: 7, - styles: PosStyles(align: PosAlign.left, bold: true), - ), - PosColumn( - text: _formatRupiah(group.total), - width: 5, - styles: PosStyles(align: PosAlign.right, bold: true), - ), - ])); + bytes.addAll(generator.text( + _padRow('Subtotal', _formatRupiah(group.total)), + styles: PosStyles(align: PosAlign.left, bold: true), + )); if (gi < groupsWithItems.length - 1) { bytes.addAll(generator.feed(1)); @@ -239,24 +233,6 @@ class EscPosPrintService { bytes.addAll(generator.feed(1)); - // Separator line - bytes.addAll(generator.text('================================', - styles: PosStyles(align: PosAlign.center))); - - // Item table header - bytes.addAll(generator.row([ - PosColumn( - text: 'Item', - width: 7, - styles: PosStyles(bold: true, align: PosAlign.left), - ), - PosColumn( - text: 'Total', - width: 5, - styles: PosStyles(bold: true, align: PosAlign.right), - ), - ])); - return bytes; } @@ -268,7 +244,6 @@ class EscPosPrintService { try { var item = items[i]; - // Item description row bytes.addAll(generator.row([ PosColumn( text: item.description, @@ -276,25 +251,11 @@ class EscPosPrintService { styles: PosStyles(align: PosAlign.left), ), ])); - - // Quantity and price row - bytes.addAll(generator.row([ - PosColumn( - text: '${item.quantity} x ${_formatRupiah(item.price)}', - width: 7, - styles: PosStyles( - align: PosAlign.left, - height: PosTextSize.size1, - width: PosTextSize.size1), - ), - PosColumn( - text: _formatRupiah(item.total), - width: 5, - styles: PosStyles(align: PosAlign.right), - ), - ])); + bytes.addAll(generator.text( + _padRow(_compactQtyPrice(item.quantity, item.price), _formatRupiahCompact(item.total)), + styles: PosStyles(align: PosAlign.left), + )); } catch (e) { - // Lanjutkan ke item berikutnya jika ada error continue; } } @@ -303,8 +264,8 @@ class EscPosPrintService { } /// Add totals and payment information - static List _addTotals(Generator generator, List groups, - PaymentInfo? paymentInfo, + static List _addTotals( + Generator generator, List groups, PaymentInfo? paymentInfo, {String? tipSourceAccountName}) { List bytes = []; @@ -313,22 +274,13 @@ class EscPosPrintService { styles: PosStyles(align: PosAlign.center))); // Calculate grand total - double totalAmount = - groups.fold(0.0, (sum, group) => sum + group.total); + double totalAmount = groups.fold(0.0, (sum, group) => sum + group.total); // Add total row - bytes.addAll(generator.row([ - PosColumn( - text: 'TOTAL', - width: 7, - styles: PosStyles(bold: true, align: PosAlign.left), - ), - PosColumn( - text: _formatRupiah(totalAmount), - width: 5, - styles: PosStyles(bold: true, align: PosAlign.right), - ), - ])); + bytes.addAll(generator.text( + _padRow('TOTAL', _formatRupiah(totalAmount)), + styles: PosStyles(bold: true, align: PosAlign.left), + )); // Add payment information if provided if (paymentInfo != null && paymentInfo.paymentAmount > 0) { @@ -336,10 +288,6 @@ class EscPosPrintService { tipSourceAccountName: tipSourceAccountName)); } - // Final separator - bytes.addAll(generator.text('================================', - styles: PosStyles(align: PosAlign.center))); - return bytes; } @@ -351,59 +299,22 @@ class EscPosPrintService { final changeAmount = paymentInfo.paymentAmount - totalAmount; // Payment amount row - bytes.addAll(generator.row([ - PosColumn( - text: 'BAYAR', - width: 7, - styles: PosStyles(align: PosAlign.left), - ), - PosColumn( - text: _formatRupiah(paymentInfo.paymentAmount), - width: 5, - styles: PosStyles(align: PosAlign.right), - ), - ])); + bytes.addAll(generator.text( + _padRow('BAYAR', _formatRupiah(paymentInfo.paymentAmount)), + styles: PosStyles(align: PosAlign.left), + )); if (changeAmount >= 0) { - // Change amount row - bytes.addAll(generator.row([ - PosColumn( - text: 'KEMBALI', - width: 7, - styles: PosStyles(align: PosAlign.left), - ), - PosColumn( - text: _formatRupiah(changeAmount), - width: 5, - styles: PosStyles(align: PosAlign.right), - ), - ])); - - // Tip information - if (paymentInfo.isTip && changeAmount > 0) { - bytes.addAll(generator.text('SEBAGAI TIP: YA', - styles: PosStyles(align: PosAlign.center, bold: true))); - if (tipSourceAccountName != null && tipSourceAccountName.isNotEmpty) { - bytes.addAll(generator.text('Sumber tip: $tipSourceAccountName', - styles: PosStyles(align: PosAlign.center))); - } - bytes.addAll(generator.text('(Uang kembalian sebagai tip)', - styles: PosStyles(align: PosAlign.center))); - } + bytes.addAll(generator.text( + _padRow(paymentInfo.isTip ? 'TIP' : 'KEMBALI', _formatRupiah(changeAmount)), + styles: PosStyles(align: PosAlign.left), + )); } else { // Amount still needed row - bytes.addAll(generator.row([ - PosColumn( - text: 'KURANG', - width: 7, - styles: PosStyles(align: PosAlign.left), - ), - PosColumn( - text: _formatRupiah(changeAmount.abs()), - width: 5, - styles: PosStyles(align: PosAlign.right), - ), - ])); + bytes.addAll(generator.text( + _padRow('KURANG', _formatRupiah(changeAmount.abs())), + styles: PosStyles(align: PosAlign.left), + )); } bytes.addAll(generator.text('--------------------------------', @@ -477,6 +388,24 @@ class EscPosPrintService { return "Rp ${formatter.format(amount)}"; } + /// Format angka compact tanpa Rp dan separator + static String _formatRupiahCompact(double amount) { + return NumberFormat("0", "id_ID").format(amount); + } + + /// Format qty x price compact: "2x65000" + static String _compactQtyPrice(double quantity, double price) { + final qtyInt = quantity.toInt(); + final priceStr = NumberFormat("0", "id_ID").format(price); + return "${qtyInt}x$priceStr"; + } + + /// Pad left/right text into one line to avoid ESC a alignment issues in row() + static String _padRow(String left, String right, {int maxChars = 32}) { + final padding = maxChars - left.length - right.length; + return left + ' ' * (padding > 0 ? padding : 1) + right; + } + /// Mencetak struk ke printer thermal static Future printToThermalPrinter({ required List groups,