fix: perbaiki item dan total pecah baris di struk thermal

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()
This commit is contained in:
a2nr 2026-06-28 21:55:17 +07:00
parent 4140af7ba0
commit 860a929da8
2 changed files with 55 additions and 130 deletions

View File

@ -460,15 +460,11 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
),
),
if (_isPrinting)
Positioned(
top: 50,
right: 16,
child: PrintingStatusCard(
isVisible: _isPrinting,
onDismiss: () {
setState(() => _isPrinting = false);
},
),
PrintingStatusCard(
isVisible: _isPrinting,
onDismiss: () {
setState(() => _isPrinting = false);
},
),
],
),

View File

@ -122,6 +122,9 @@ class EscPosPrintService {
// Generate all receipt sections
List<int> 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<int> _addTotals(Generator generator, List<ReceiptGroup> groups,
PaymentInfo? paymentInfo,
static List<int> _addTotals(
Generator generator, List<ReceiptGroup> groups, PaymentInfo? paymentInfo,
{String? tipSourceAccountName}) {
List<int> 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<void> printToThermalPrinter({
required List<ReceiptGroup> groups,