print receipt
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
|
||||
|
||||
import '../../../common/extension/extension.dart';
|
||||
import '../../../domain/order/order.dart';
|
||||
import '../../../domain/outlet/outlet.dart';
|
||||
import 'receipt_component_builder.dart';
|
||||
|
||||
class PrintUi {
|
||||
Future<List<int>> printOrder({
|
||||
required Order order,
|
||||
required Outlet outlet,
|
||||
required String cashierName,
|
||||
int paper = 58,
|
||||
}) async {
|
||||
List<int> bytes = [];
|
||||
|
||||
final profile = await CapabilityProfile.load();
|
||||
final generator = Generator(
|
||||
paper == 58 ? PaperSize.mm58 : PaperSize.mm80,
|
||||
profile,
|
||||
);
|
||||
final builder = ReceiptComponentBuilder(
|
||||
generator: generator,
|
||||
paperSize: 58,
|
||||
);
|
||||
|
||||
bytes += generator.reset();
|
||||
|
||||
bytes += builder.header(
|
||||
outletName: outlet.name,
|
||||
address: outlet.address,
|
||||
phoneNumber: outlet.phoneNumber,
|
||||
);
|
||||
|
||||
bytes += builder.dateTime(DateTime.now());
|
||||
|
||||
bytes += builder.orderInfo(
|
||||
orderNumber: order.orderNumber,
|
||||
customerName: order.metadata['customer_name'] ?? 'John Doe',
|
||||
cashierName: cashierName,
|
||||
paymentMethod: order.payments.last.paymentMethodName,
|
||||
tableNumber: order.tableNumber,
|
||||
);
|
||||
|
||||
bytes += builder.orderType(order.orderType);
|
||||
|
||||
bytes += builder.emptyLines(1);
|
||||
|
||||
for (final item in order.orderItems) {
|
||||
bytes += builder.orderItem(
|
||||
productName: item.productName,
|
||||
quantity: item.quantity,
|
||||
unitPrice: item.unitPrice.currencyFormatRpV2,
|
||||
totalPrice: item.totalPrice.currencyFormatRpV2,
|
||||
variantName: item.productVariantName,
|
||||
notes: item.notes,
|
||||
);
|
||||
}
|
||||
|
||||
bytes += builder.summary(
|
||||
totalItems: order.orderItems.length,
|
||||
subtotal: order.subtotal.currencyFormatRpV2,
|
||||
discount: order.discountAmount.currencyFormatRpV2,
|
||||
total: order.totalAmount.currencyFormatRpV2,
|
||||
paid: order.totalPaid.currencyFormatRpV2,
|
||||
);
|
||||
|
||||
bytes += builder.footer();
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import 'package:esc_pos_utils_plus/esc_pos_utils_plus.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});
|
||||
|
||||
/// Get separator line based on paper size
|
||||
String get _separator => paperSize == 80
|
||||
? '------------------------------------------------'
|
||||
: '--------------------------------';
|
||||
|
||||
/// Print text centered with custom style
|
||||
List<int> 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Print text aligned left
|
||||
List<int> textLeft(String text, {bool bold = false}) {
|
||||
return generator.text(
|
||||
text,
|
||||
styles: PosStyles(bold: bold, align: PosAlign.left),
|
||||
);
|
||||
}
|
||||
|
||||
/// Print text aligned right
|
||||
List<int> textRight(String text, {bool bold = false}) {
|
||||
return generator.text(
|
||||
text,
|
||||
styles: PosStyles(bold: bold, align: PosAlign.right),
|
||||
);
|
||||
}
|
||||
|
||||
/// Print separator line
|
||||
List<int> separator({bool bold = false}) {
|
||||
return generator.text(
|
||||
_separator,
|
||||
styles: PosStyles(bold: bold, align: PosAlign.center),
|
||||
);
|
||||
}
|
||||
|
||||
/// Print row with 2 columns (label: value)
|
||||
List<int> 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),
|
||||
),
|
||||
PosColumn(
|
||||
text: rightText,
|
||||
width: rightWidth,
|
||||
styles: PosStyles(align: PosAlign.right, bold: bold),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Print row with 3 columns
|
||||
List<int> 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),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/// Print empty lines
|
||||
List<int> emptyLines(int count) {
|
||||
return generator.emptyLines(count);
|
||||
}
|
||||
|
||||
/// Print feed lines
|
||||
List<int> feed(int count) {
|
||||
return generator.feed(count);
|
||||
}
|
||||
|
||||
/// Print header (outlet info)
|
||||
List<int> header({
|
||||
required String outletName,
|
||||
required String address,
|
||||
required String phoneNumber,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
|
||||
bytes += textCenter(
|
||||
outletName,
|
||||
bold: true,
|
||||
height: PosTextSize.size1,
|
||||
width: PosTextSize.size1,
|
||||
);
|
||||
bytes += textCenter(address);
|
||||
bytes += textCenter(phoneNumber);
|
||||
bytes += separator();
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// Print date and time
|
||||
List<int> dateTime(DateTime dateTime) {
|
||||
return row2Columns(
|
||||
DateFormat('dd MMM yyyy').format(dateTime),
|
||||
DateFormat('HH:mm').format(dateTime),
|
||||
);
|
||||
}
|
||||
|
||||
/// Print order info section
|
||||
List<int> orderInfo({
|
||||
required String orderNumber,
|
||||
required String customerName,
|
||||
required String cashierName,
|
||||
String? paymentMethod,
|
||||
String? tableNumber,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
|
||||
bytes += row2Columns('No. Pesanan', orderNumber);
|
||||
bytes += row2Columns('Pelanggan', customerName);
|
||||
bytes += row2Columns('Kasir', cashierName);
|
||||
|
||||
if (paymentMethod != null) {
|
||||
bytes += row2Columns(
|
||||
'Pembayaran',
|
||||
paymentMethod,
|
||||
leftWidth: 8,
|
||||
rightWidth: 4,
|
||||
);
|
||||
}
|
||||
|
||||
if (tableNumber != null && tableNumber.isNotEmpty) {
|
||||
bytes += row2Columns('Meja', tableNumber, leftWidth: 8, rightWidth: 4);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// Print order type (Dine In, Take Away, etc)
|
||||
List<int> orderType(String type) {
|
||||
List<int> bytes = [];
|
||||
|
||||
bytes += separator();
|
||||
bytes += textCenter(type, bold: true);
|
||||
bytes += separator();
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// Print single item
|
||||
List<int> orderItem({
|
||||
required String productName,
|
||||
required int quantity,
|
||||
required String unitPrice,
|
||||
required String totalPrice,
|
||||
String? variantName,
|
||||
String? notes,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
|
||||
final variantText = variantName != null && variantName.isNotEmpty
|
||||
? "($variantName)"
|
||||
: '';
|
||||
|
||||
bytes += textLeft('$productName $variantText', bold: true);
|
||||
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;
|
||||
}
|
||||
|
||||
/// Print summary section
|
||||
List<int> summary({
|
||||
required int totalItems,
|
||||
required String subtotal,
|
||||
required String discount,
|
||||
required String total,
|
||||
required String paid,
|
||||
}) {
|
||||
List<int> bytes = [];
|
||||
|
||||
bytes += separator();
|
||||
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;
|
||||
}
|
||||
|
||||
/// Print footer (thank you message)
|
||||
List<int> footer({String message = 'Terima kasih'}) {
|
||||
List<int> bytes = [];
|
||||
|
||||
bytes += emptyLines(2);
|
||||
bytes += textCenter(
|
||||
message,
|
||||
bold: true,
|
||||
height: PosTextSize.size1,
|
||||
width: PosTextSize.size1,
|
||||
);
|
||||
bytes += feed(paperSize == 80 ? 3 : 1);
|
||||
bytes += generator.cut();
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// Print QR Code
|
||||
List<int> qrCode(String data, {PosAlign align = PosAlign.center}) {
|
||||
return generator.qrcode(data, align: align);
|
||||
}
|
||||
|
||||
/// Print barcode
|
||||
// List<int> barcode(String data, {BarcodeType type = BarcodeType.code128}) {
|
||||
// return generator.barcode(
|
||||
// Barcode.code128(data),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
@@ -101,6 +101,23 @@ class _SettingPrinterFormState extends State<SettingPrinterForm> {
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocListener<PrinterFormBloc, PrinterFormState>(
|
||||
listenWhen: (p, c) => p.failureOrPrintTest != c.failureOrPrintTest,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintTest.fold(
|
||||
() => null,
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(either) {
|
||||
AppFlushbar.showSuccess(
|
||||
context,
|
||||
'Printer ${widget.code.toTitleCase()} test berhasil',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<PrinterFormBloc, PrinterFormState>(
|
||||
builder: (context, state) {
|
||||
@@ -187,6 +204,20 @@ class _SettingPrinterFormState extends State<SettingPrinterForm> {
|
||||
),
|
||||
SpaceWidth(12),
|
||||
],
|
||||
Expanded(
|
||||
child: AppElevatedButton.outlined(
|
||||
onPressed: () {
|
||||
context.read<PrinterFormBloc>().add(
|
||||
PrinterFormEvent.printTest(
|
||||
code: widget.code,
|
||||
macAccdress: state.address,
|
||||
),
|
||||
);
|
||||
},
|
||||
label: 'Test Print',
|
||||
),
|
||||
),
|
||||
SpaceWidth(12),
|
||||
Expanded(
|
||||
child: AppElevatedButton.filled(
|
||||
onPressed: () {
|
||||
|
||||
+48
-22
@@ -29,29 +29,47 @@ class _SettingPrinterReceiptState extends State<SettingPrinterReceipt> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<PrinterFormBloc, PrinterFormState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.failureOrDeleteSuccess != c.failureOrDeleteSuccess,
|
||||
listener: (context, state) {
|
||||
state.failureOrDeleteSuccess.fold(
|
||||
() => null,
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(_) {
|
||||
if (context.mounted) {
|
||||
context.read<PrinterLoaderBloc>().add(
|
||||
const PrinterLoaderEvent.getByCode('receipt'),
|
||||
);
|
||||
}
|
||||
return MultiBlocListener(
|
||||
listeners: [
|
||||
BlocListener<PrinterFormBloc, PrinterFormState>(
|
||||
listenWhen: (p, c) =>
|
||||
p.failureOrDeleteSuccess != c.failureOrDeleteSuccess,
|
||||
listener: (context, state) {
|
||||
state.failureOrDeleteSuccess.fold(
|
||||
() => null,
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(_) {
|
||||
if (context.mounted) {
|
||||
context.read<PrinterLoaderBloc>().add(
|
||||
const PrinterLoaderEvent.getByCode('receipt'),
|
||||
);
|
||||
}
|
||||
|
||||
AppFlushbar.showSuccess(
|
||||
context,
|
||||
'Printer receipt berhasil dihapus',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
AppFlushbar.showSuccess(
|
||||
context,
|
||||
'Printer receipt berhasil dihapus',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
BlocListener<PrinterFormBloc, PrinterFormState>(
|
||||
listenWhen: (p, c) => p.failureOrPrintTest != c.failureOrPrintTest,
|
||||
listener: (context, state) {
|
||||
state.failureOrPrintTest.fold(
|
||||
() => null,
|
||||
(either) => either.fold(
|
||||
(f) => AppFlushbar.showPrinterFailureToast(context, f),
|
||||
(either) {
|
||||
AppFlushbar.showSuccess(context, 'Printer test berhasil');
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Material(
|
||||
color: AppColor.background,
|
||||
child: SingleChildScrollView(
|
||||
@@ -106,6 +124,14 @@ class _SettingPrinterReceiptState extends State<SettingPrinterReceipt> {
|
||||
PrinterFormEvent.deleted(state.printer.id),
|
||||
);
|
||||
},
|
||||
onTestPrint: () {
|
||||
context.read<PrinterFormBloc>().add(
|
||||
PrinterFormEvent.printTest(
|
||||
code: state.printer.code,
|
||||
macAccdress: state.printer.address,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
(f) => f.maybeMap(
|
||||
orElse: () => ErrorCard(
|
||||
|
||||
Reference in New Issue
Block a user