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:
parent
4140af7ba0
commit
860a929da8
|
|
@ -460,15 +460,11 @@ class _ReceiptScreenState extends State<ReceiptScreen> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_isPrinting)
|
if (_isPrinting)
|
||||||
Positioned(
|
PrintingStatusCard(
|
||||||
top: 50,
|
isVisible: _isPrinting,
|
||||||
right: 16,
|
onDismiss: () {
|
||||||
child: PrintingStatusCard(
|
setState(() => _isPrinting = false);
|
||||||
isVisible: _isPrinting,
|
},
|
||||||
onDismiss: () {
|
|
||||||
setState(() => _isPrinting = false);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -122,6 +122,9 @@ class EscPosPrintService {
|
||||||
// Generate all receipt sections
|
// Generate all receipt sections
|
||||||
List<int> bytes = [];
|
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)
|
// Add store logo if available (diproses di isolate terpisah)
|
||||||
bytes.addAll(await _addStoreLogo(generator, storeConfig.logoPath));
|
bytes.addAll(await _addStoreLogo(generator, storeConfig.logoPath));
|
||||||
|
|
||||||
|
|
@ -137,26 +140,17 @@ class EscPosPrintService {
|
||||||
// Header grup: satu garis + nama sumber (tanpa "Dari:")
|
// Header grup: satu garis + nama sumber (tanpa "Dari:")
|
||||||
bytes.addAll(generator.text('--------------------------------',
|
bytes.addAll(generator.text('--------------------------------',
|
||||||
styles: PosStyles(align: PosAlign.center)));
|
styles: PosStyles(align: PosAlign.center)));
|
||||||
bytes.addAll(generator.text(
|
bytes.addAll(generator.text(group.sourceAccountName ?? '(belum dipilih)',
|
||||||
group.sourceAccountName ?? '(belum dipilih)',
|
styles: PosStyles(align: PosAlign.center, bold: true)));
|
||||||
styles: PosStyles(align: PosAlign.left, bold: true)));
|
|
||||||
|
|
||||||
// Item list grup
|
// Item list grup
|
||||||
bytes.addAll(_addItemList(generator, group.items));
|
bytes.addAll(_addItemList(generator, group.items));
|
||||||
|
|
||||||
// Subtotal grup
|
// Subtotal grup
|
||||||
bytes.addAll(generator.row([
|
bytes.addAll(generator.text(
|
||||||
PosColumn(
|
_padRow('Subtotal', _formatRupiah(group.total)),
|
||||||
text: 'Subtotal',
|
styles: PosStyles(align: PosAlign.left, bold: true),
|
||||||
width: 7,
|
));
|
||||||
styles: PosStyles(align: PosAlign.left, bold: true),
|
|
||||||
),
|
|
||||||
PosColumn(
|
|
||||||
text: _formatRupiah(group.total),
|
|
||||||
width: 5,
|
|
||||||
styles: PosStyles(align: PosAlign.right, bold: true),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
|
|
||||||
if (gi < groupsWithItems.length - 1) {
|
if (gi < groupsWithItems.length - 1) {
|
||||||
bytes.addAll(generator.feed(1));
|
bytes.addAll(generator.feed(1));
|
||||||
|
|
@ -239,24 +233,6 @@ class EscPosPrintService {
|
||||||
|
|
||||||
bytes.addAll(generator.feed(1));
|
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;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -268,7 +244,6 @@ class EscPosPrintService {
|
||||||
try {
|
try {
|
||||||
var item = items[i];
|
var item = items[i];
|
||||||
|
|
||||||
// Item description row
|
|
||||||
bytes.addAll(generator.row([
|
bytes.addAll(generator.row([
|
||||||
PosColumn(
|
PosColumn(
|
||||||
text: item.description,
|
text: item.description,
|
||||||
|
|
@ -276,25 +251,11 @@ class EscPosPrintService {
|
||||||
styles: PosStyles(align: PosAlign.left),
|
styles: PosStyles(align: PosAlign.left),
|
||||||
),
|
),
|
||||||
]));
|
]));
|
||||||
|
bytes.addAll(generator.text(
|
||||||
// Quantity and price row
|
_padRow(_compactQtyPrice(item.quantity, item.price), _formatRupiahCompact(item.total)),
|
||||||
bytes.addAll(generator.row([
|
styles: PosStyles(align: PosAlign.left),
|
||||||
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),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Lanjutkan ke item berikutnya jika ada error
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -303,8 +264,8 @@ class EscPosPrintService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add totals and payment information
|
/// Add totals and payment information
|
||||||
static List<int> _addTotals(Generator generator, List<ReceiptGroup> groups,
|
static List<int> _addTotals(
|
||||||
PaymentInfo? paymentInfo,
|
Generator generator, List<ReceiptGroup> groups, PaymentInfo? paymentInfo,
|
||||||
{String? tipSourceAccountName}) {
|
{String? tipSourceAccountName}) {
|
||||||
List<int> bytes = [];
|
List<int> bytes = [];
|
||||||
|
|
||||||
|
|
@ -313,22 +274,13 @@ class EscPosPrintService {
|
||||||
styles: PosStyles(align: PosAlign.center)));
|
styles: PosStyles(align: PosAlign.center)));
|
||||||
|
|
||||||
// Calculate grand total
|
// Calculate grand total
|
||||||
double totalAmount =
|
double totalAmount = groups.fold(0.0, (sum, group) => sum + group.total);
|
||||||
groups.fold(0.0, (sum, group) => sum + group.total);
|
|
||||||
|
|
||||||
// Add total row
|
// Add total row
|
||||||
bytes.addAll(generator.row([
|
bytes.addAll(generator.text(
|
||||||
PosColumn(
|
_padRow('TOTAL', _formatRupiah(totalAmount)),
|
||||||
text: 'TOTAL',
|
styles: PosStyles(bold: true, align: PosAlign.left),
|
||||||
width: 7,
|
));
|
||||||
styles: PosStyles(bold: true, align: PosAlign.left),
|
|
||||||
),
|
|
||||||
PosColumn(
|
|
||||||
text: _formatRupiah(totalAmount),
|
|
||||||
width: 5,
|
|
||||||
styles: PosStyles(bold: true, align: PosAlign.right),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
|
|
||||||
// Add payment information if provided
|
// Add payment information if provided
|
||||||
if (paymentInfo != null && paymentInfo.paymentAmount > 0) {
|
if (paymentInfo != null && paymentInfo.paymentAmount > 0) {
|
||||||
|
|
@ -336,10 +288,6 @@ class EscPosPrintService {
|
||||||
tipSourceAccountName: tipSourceAccountName));
|
tipSourceAccountName: tipSourceAccountName));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Final separator
|
|
||||||
bytes.addAll(generator.text('================================',
|
|
||||||
styles: PosStyles(align: PosAlign.center)));
|
|
||||||
|
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,59 +299,22 @@ class EscPosPrintService {
|
||||||
final changeAmount = paymentInfo.paymentAmount - totalAmount;
|
final changeAmount = paymentInfo.paymentAmount - totalAmount;
|
||||||
|
|
||||||
// Payment amount row
|
// Payment amount row
|
||||||
bytes.addAll(generator.row([
|
bytes.addAll(generator.text(
|
||||||
PosColumn(
|
_padRow('BAYAR', _formatRupiah(paymentInfo.paymentAmount)),
|
||||||
text: 'BAYAR',
|
styles: PosStyles(align: PosAlign.left),
|
||||||
width: 7,
|
));
|
||||||
styles: PosStyles(align: PosAlign.left),
|
|
||||||
),
|
|
||||||
PosColumn(
|
|
||||||
text: _formatRupiah(paymentInfo.paymentAmount),
|
|
||||||
width: 5,
|
|
||||||
styles: PosStyles(align: PosAlign.right),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
|
|
||||||
if (changeAmount >= 0) {
|
if (changeAmount >= 0) {
|
||||||
// Change amount row
|
bytes.addAll(generator.text(
|
||||||
bytes.addAll(generator.row([
|
_padRow(paymentInfo.isTip ? 'TIP' : 'KEMBALI', _formatRupiah(changeAmount)),
|
||||||
PosColumn(
|
styles: PosStyles(align: PosAlign.left),
|
||||||
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)));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Amount still needed row
|
// Amount still needed row
|
||||||
bytes.addAll(generator.row([
|
bytes.addAll(generator.text(
|
||||||
PosColumn(
|
_padRow('KURANG', _formatRupiah(changeAmount.abs())),
|
||||||
text: 'KURANG',
|
styles: PosStyles(align: PosAlign.left),
|
||||||
width: 7,
|
));
|
||||||
styles: PosStyles(align: PosAlign.left),
|
|
||||||
),
|
|
||||||
PosColumn(
|
|
||||||
text: _formatRupiah(changeAmount.abs()),
|
|
||||||
width: 5,
|
|
||||||
styles: PosStyles(align: PosAlign.right),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes.addAll(generator.text('--------------------------------',
|
bytes.addAll(generator.text('--------------------------------',
|
||||||
|
|
@ -477,6 +388,24 @@ class EscPosPrintService {
|
||||||
return "Rp ${formatter.format(amount)}";
|
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
|
/// Mencetak struk ke printer thermal
|
||||||
static Future<void> printToThermalPrinter({
|
static Future<void> printToThermalPrinter({
|
||||||
required List<ReceiptGroup> groups,
|
required List<ReceiptGroup> groups,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue