import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:intl/intl.dart'; /// Reusable component builder untuk thermal receipt printer class ReceiptComponentBuilder { final Generator generator; final int paperSize; ReceiptComponentBuilder({required this.generator, this.paperSize = 58}); /// Helper: returns size2 for 80mm paper, size1 for 58mm PosTextSize get _titleSize => paperSize == 80 ? PosTextSize.size3 : PosTextSize.size1; /// Font type per paper size — easy to change here /// 58mm → fontA, 80mm → fontA PosFontType get _font => paperSize == 80 ? PosFontType.fontA : PosFontType.fontA; /// Text size for body — height size2 for 80mm to appear taller, size1 for 58mm PosTextSize get _bodySize => paperSize == 80 ? PosTextSize.size2 : PosTextSize.size1; /// Body width stays size1 always to prevent overflow PosTextSize get _bodyWidth => PosTextSize.size1; /// Characters per line based on paper size (always size1 font) String get _separatorLine => paperSize == 80 ? '------------------------------------------------' : '--------------------------------'; // --------------------------------------------------------------------------- // Basic text // --------------------------------------------------------------------------- List textCenter( String text, { bool bold = false, PosTextSize height = PosTextSize.size1, PosTextSize width = PosTextSize.size1, }) { return generator.text( text, styles: PosStyles( bold: bold, align: PosAlign.center, height: height, width: width, fontType: _font, ), ); } List textLeft(String text, {bool bold = false}) { return generator.text( text, styles: PosStyles( bold: bold, align: PosAlign.left, fontType: _font, height: _bodySize, width: _bodyWidth, ), ); } List textRight(String text, {bool bold = false}) { return generator.text( text, styles: PosStyles( bold: bold, align: PosAlign.right, fontType: _font, height: _bodySize, width: _bodyWidth, ), ); } // --------------------------------------------------------------------------- // Layout helpers // --------------------------------------------------------------------------- List separator({bool bold = false}) { return generator.text( _separatorLine, styles: PosStyles( bold: bold, align: PosAlign.center, height: PosTextSize.size1, width: PosTextSize.size1, fontType: _font, ), ); } List row2Columns( String leftText, String rightText, { bool bold = false, int leftWidth = 6, int rightWidth = 6, }) { return generator.row([ PosColumn( text: leftText, width: leftWidth, styles: PosStyles( align: PosAlign.left, bold: bold, fontType: _font, height: _bodySize, width: _bodyWidth, ), ), PosColumn( text: rightText, width: rightWidth, styles: PosStyles( align: PosAlign.right, bold: bold, fontType: _font, height: _bodySize, width: _bodyWidth, ), ), ]); } List row3Columns( String leftText, String centerText, String rightText, { int leftWidth = 4, int centerWidth = 4, int rightWidth = 4, }) { return generator.row([ PosColumn( text: leftText, width: leftWidth, styles: const PosStyles(align: PosAlign.left), ), PosColumn( text: centerText, width: centerWidth, styles: const PosStyles(align: PosAlign.center), ), PosColumn( text: rightText, width: rightWidth, styles: const PosStyles(align: PosAlign.right), ), ]); } /// Item text — always size2 for kitchen/checker (prominent display) List itemText(String text, {bool bold = true}) { return generator.text( text, styles: PosStyles( bold: bold, align: PosAlign.left, fontType: _font, height: PosTextSize.size2, width: PosTextSize.size2, ), ); } List emptyLines(int count) => generator.emptyLines(count); List feed(int count) => generator.feed(count); // --------------------------------------------------------------------------- // Composite components // --------------------------------------------------------------------------- /// Outlet header (receipt/cashier style) List header({ required String outletName, required String address, required String phoneNumber, }) { List bytes = []; bytes += textCenter(outletName, bold: true, height: _titleSize, width: _titleSize); bytes += textCenter(address); bytes += textCenter(phoneNumber); bytes += separator(); return bytes; } /// Centered printer type label (e.g. KITCHEN, BAR) List printerType({required String printerType}) { return textCenter(printerType, bold: true, height: _titleSize, width: _titleSize); } /// Date + time row (receipt style) List dateTime(DateTime dateTime) { return row2Columns( DateFormat('dd MMM yyyy').format(dateTime), DateFormat('HH:mm').format(dateTime), ); } /// Order info block — label : value style (checker/kitchen style) List orderInfoSimple({ required String orderNumber, String? orderType, required String cashierName, }) { List bytes = []; final dateStr = DateFormat('dd-MM-yyyy HH:mm').format(DateTime.now()); bytes += textLeft('Order : $orderNumber'); bytes += textLeft('Date : $dateStr'); if(orderType != null) { bytes += textLeft('Purpose : $orderType'); } bytes += textLeft('Waiter : $cashierName'); return bytes; } /// Order info block — row2Columns style (receipt/cashier style) List orderInfo({ required String orderNumber, required String customerName, required String cashierName, String? paymentMethod, String? tableNumber, }) { List bytes = []; final dateStr = DateFormat('dd-MM-yyyy HH:mm').format(DateTime.now()); bytes += textLeft('Order : $orderNumber'); bytes += textLeft('Date : $dateStr'); if (tableNumber != null && tableNumber.isNotEmpty) { bytes += textLeft('Table : $tableNumber'); } bytes += textLeft('Waiter : $cashierName'); bytes += textLeft('Customer : $customerName'); if (paymentMethod != null) { bytes += textLeft('Payment : $paymentMethod'); } return bytes; } /// Order type banner (separator + type + separator) List orderType(String type) { List bytes = []; bytes += separator(); bytes += textCenter(type, bold: true, height: _titleSize, width: _titleSize); bytes += separator(); return bytes; } /// Single item row with price (receipt/cashier style) List orderItem({ required String productName, required int quantity, required String unitPrice, required String totalPrice, String? variantName, String? notes, }) { List bytes = []; final displayName = (variantName != null && variantName.isNotEmpty) ? '$productName ($variantName)' : productName; bytes += textLeft(displayName, bold: paperSize == 80); bytes += row2Columns('$quantity x $unitPrice', totalPrice, leftWidth: 8, rightWidth: 4); if (notes != null && notes.isNotEmpty) { bytes += row2Columns('Note', notes, leftWidth: 4, rightWidth: 8); } bytes += emptyLines(1); return bytes; } /// Summary section List summary({ required int totalItems, required String subtotal, required String discount, required String total, required String paid, }) { List bytes = []; bytes += separator(); if (totalItems > 0) { bytes += row2Columns('Total Item', totalItems.toString()); } bytes += row2Columns('Subtotal', subtotal); bytes += row2Columns('Diskon', discount); bytes += separator(); bytes += row2Columns('Total', total, bold: true); bytes += row2Columns('Bayar', paid); bytes += separator(); return bytes; } /// Footer with thank-you message + cut List footer({String message = 'Terima kasih'}) { List bytes = []; bytes += emptyLines(2); bytes += textCenter(message, bold: true, height: _titleSize, width: _titleSize); if (kDebugMode) { bytes += textCenter('$paperSize MM'); } bytes += feed(paperSize == 80 ? 3 : 1); bytes += generator.cut(); return bytes; } /// Feed + cut without any message List cutOnly() { List bytes = []; bytes += feed(paperSize == 80 ? 3 : 1); bytes += generator.cut(); return bytes; } // --------------------------------------------------------------------------- // Media // --------------------------------------------------------------------------- List qrCode(String data, {PosAlign align = PosAlign.center}) { return generator.qrcode(data, align: align); } List barcode(String data) { return generator.barcode(Barcode.code128(data.codeUnits)); } }