cashumit/lib/services/esc_pos_print_service.dart

508 lines
16 KiB
Dart

import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';
import 'package:cashumit/models/receipt_item.dart';
import 'package:flutter_esc_pos_utils/flutter_esc_pos_utils.dart';
import 'package:image/image.dart' as img;
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:cashumit/services/print_config.dart';
/// Data class untuk konfigurasi toko
class StoreConfig {
final String name;
final String address;
final String adminName;
final String adminPhone;
final String? logoPath;
final String disclaimer;
final String thankYouText;
final String pantunText;
StoreConfig({
required this.name,
required this.address,
required this.adminName,
required this.adminPhone,
this.logoPath,
required this.disclaimer,
required this.thankYouText,
required this.pantunText,
});
static Future<StoreConfig> fromSharedPreferences() async {
final prefs = await SharedPreferences.getInstance();
// Baca semua nilai sekaligus untuk menghindari multiple disk reads
final storeName = prefs.getString('store_name') ?? 'TOKO SEMBAKO MURAH';
final storeAddress =
prefs.getString('store_address') ?? 'Jl. Merdeka No. 123';
final adminName = prefs.getString('admin_name') ?? 'Budi Santoso';
final adminPhone = prefs.getString('admin_phone') ?? '08123456789';
final logoPath = prefs.getString('store_logo_path');
final disclaimer = prefs.getString('store_disclaimer_text') ??
'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. '
'Harap periksa kembali struk belanja Anda sebelum meninggalkan toko.';
final thankYouText =
prefs.getString('thank_you_text') ?? '*** TERIMA KASIH ***';
final pantunText = prefs.getString('pantun_text') ??
'Belanja di toko kami, hemat dan nyaman\n'
'Dengan penuh semangat, kami siap melayani\n'
'Harapan kami, Anda selalu puas\n'
'Sampai jumpa lagi, selamat tinggal.';
return StoreConfig(
name: storeName,
address: storeAddress,
adminName: adminName,
adminPhone: adminPhone,
logoPath: logoPath,
disclaimer: disclaimer,
thankYouText: thankYouText,
pantunText: pantunText,
);
}
}
/// Data class untuk informasi pembayaran
class PaymentInfo {
final double paymentAmount;
final bool isTip;
PaymentInfo({
this.paymentAmount = 0.0,
this.isTip = false,
});
}
/// Service untuk menghasilkan perintah ESC/POS menggunakan flutter_esc_pos_utils
class EscPosPrintService {
/// Menghasilkan struk dalam format byte array berdasarkan data transaksi
static Future<List<int>> generateEscPosBytes({
required List<ReceiptItem> items,
required DateTime transactionDate,
PaymentInfo? paymentInfo,
}) async {
print('Memulai generateEscPosBytes...');
print('Jumlah item: ${items.length}');
print('Tanggal transaksi: $transactionDate');
// Load store info from shared preferences once
final storeConfig = await StoreConfig.fromSharedPreferences();
// Format tanggal
final dateFormatter = DateFormat('dd/MM/yyyy HH:mm');
final formattedDate = dateFormatter.format(transactionDate);
print('Tanggal yang diformat: $formattedDate');
// Load capability profile with timeout
final profile = await _loadCapabilityProfile();
// Get printer configuration
final config = await PrintConfig.loadPrinterConfig();
final paperSize = PrintConfig.getPaperSizeEnum(config['paperSize']);
// Use configured paper size or default
final generator = Generator(
paperSize == 'mm80' ? PaperSize.mm80 : PaperSize.mm58, profile);
// Generate all receipt sections
List<int> bytes = [];
// Add store logo if available
bytes.addAll(await _addStoreLogo(generator, storeConfig.logoPath));
// Add store header
bytes.addAll(_addStoreHeader(generator, storeConfig, formattedDate));
// Add item list
bytes.addAll(_addItemList(generator, items));
// Add totals
bytes.addAll(_addTotals(generator, items, paymentInfo));
// Add footer information
bytes.addAll(_addFooter(generator, storeConfig));
// Add cut command
bytes.addAll(generator.feed(2));
bytes.addAll(generator.cut());
print('Jumlah byte yang dihasilkan: ${bytes.length}');
return bytes;
}
/// Load capability profile safely
static Future<CapabilityProfile> _loadCapabilityProfile() async {
try {
return await CapabilityProfile.load();
} catch (e) {
print('Gagal memuat capability profile: $e');
// Gunakan profile default jika gagal
return await CapabilityProfile.load();
}
}
/// Add store logo to receipt
static Future<List<int>> _addStoreLogo(
Generator generator, String? logoPath) async {
List<int> bytes = [];
if (logoPath != null && logoPath.isNotEmpty) {
try {
final img.Image? logo = await _getImageFromFilePath(logoPath);
if (logo != null) {
// Mengubah ukuran logo agar pas di struk (lebar maks 300px)
final resizedLogo = img.copyResize(logo, width: 300);
bytes.addAll(generator.image(resizedLogo, align: PosAlign.center));
bytes.addAll(generator.feed(1)); // Beri sedikit spasi setelah logo
}
} catch (e) {
print('Error loading or processing store logo: $e');
}
}
return bytes;
}
/// Add store header information
static List<int> _addStoreHeader(
Generator generator, StoreConfig config, String formattedDate) {
List<int> bytes = [];
// Add store name as header
bytes.addAll(generator.text(config.name,
styles: PosStyles(
bold: true,
height: PosTextSize.size1,
width: PosTextSize.size1,
align: PosAlign.center,
)));
bytes.addAll(generator.text(config.address,
styles: PosStyles(align: PosAlign.center)));
bytes.addAll(generator.text('Admin: ${config.adminName}',
styles: PosStyles(align: PosAlign.center)));
bytes.addAll(generator.text('Telp: ${config.adminPhone}',
styles: PosStyles(align: PosAlign.center)));
bytes.addAll(generator.text(formattedDate,
styles: PosStyles(align: PosAlign.center)));
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;
}
/// Add item list to receipt
static List<int> _addItemList(Generator generator, List<ReceiptItem> items) {
List<int> bytes = [];
print('Memulai iterasi item...');
for (int i = 0; i < items.length; i++) {
try {
var item = items[i];
print(
'Item $i: ${item.description}, qty: ${item.quantity}, price: ${item.price}, total: ${item.total}');
// Item description row
bytes.addAll(generator.row([
PosColumn(
text: item.description,
width: 12,
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),
),
]));
} catch (e) {
print('Error saat memproses item $i: $e');
// Lanjutkan ke item berikutnya jika ada error
continue;
}
}
print('Selesai iterasi item');
return bytes;
}
/// Add totals and payment information
static List<int> _addTotals(
Generator generator, List<ReceiptItem> items, PaymentInfo? paymentInfo) {
List<int> bytes = [];
// Separator before total
bytes.addAll(generator.text('--------------------------------',
styles: PosStyles(align: PosAlign.center)));
// Calculate total
double totalAmount = items.fold(0.0, (sum, item) => sum + item.total);
print('Total amount: $totalAmount');
// 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),
),
]));
// Add payment information if provided
if (paymentInfo != null && paymentInfo.paymentAmount > 0) {
bytes.addAll(_addPaymentInfo(generator, totalAmount, paymentInfo));
}
// Final separator
bytes.addAll(generator.text('================================',
styles: PosStyles(align: PosAlign.center)));
return bytes;
}
/// Add payment information including change and tip
static List<int> _addPaymentInfo(
Generator generator, double totalAmount, PaymentInfo paymentInfo) {
List<int> bytes = [];
final changeAmount = paymentInfo.paymentAmount - totalAmount;
// Payment amount row - total width should be 12
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),
),
]));
if (changeAmount >= 0) {
// Change amount row - total width should be 12
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)));
bytes.addAll(generator.text('(Uang kembalian sebagai tip)',
styles: PosStyles(align: PosAlign.center)));
} else if (changeAmount > 0) {
bytes.addAll(generator.text('SEBAGAI TIP: TIDAK',
styles: PosStyles(align: PosAlign.center)));
}
} else {
// Amount still needed row - total width should be 12
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('--------------------------------',
styles: PosStyles(align: PosAlign.center)));
return bytes;
}
/// Add footer information including disclaimer and thanks
static List<int> _addFooter(Generator generator, StoreConfig config) {
List<int> bytes = [];
// Add disclaimer
bytes.addAll(generator.feed(1));
bytes.addAll(_wrapAndAddText(generator, config.disclaimer));
// Add thank you message
bytes.addAll(generator.feed(1));
bytes.addAll(generator.text(config.thankYouText,
styles: PosStyles(align: PosAlign.center, bold: true)));
// Add pantun if exists
if (config.pantunText.isNotEmpty) {
bytes.addAll(generator.feed(1));
final pantunLines = config.pantunText.split('\n');
for (final line in pantunLines) {
bytes.addAll(
generator.text(line, styles: PosStyles(align: PosAlign.center)));
}
}
return bytes;
}
/// Wrap long text into multiple lines
static List<int> _wrapAndAddText(Generator generator, String text) {
List<int> bytes = [];
try {
final words = text.split(' ');
final List<String> wrappedLines = [];
String currentLine = '';
for (final word in words) {
if ((currentLine + word).length > 32) {
wrappedLines.add(currentLine.trim());
currentLine = word + ' ';
} else {
currentLine += word + ' ';
}
}
if (currentLine.trim().isNotEmpty) {
wrappedLines.add(currentLine.trim());
}
for (final line in wrappedLines) {
bytes.addAll(
generator.text(line, styles: PosStyles(align: PosAlign.center)));
}
} catch (e) {
print('Error saat memproses teks: $e');
// Fallback jika ada error
bytes.addAll(
generator.text(text, styles: PosStyles(align: PosAlign.center)));
}
return bytes;
}
/// Format angka ke rupiah
static String _formatRupiah(double amount) {
final formatter = NumberFormat("#,##0", "id_ID");
return "Rp ${formatter.format(amount)}";
}
/// Mencetak struk ke printer thermal
static Future<void> printToThermalPrinter({
required List<ReceiptItem> items,
required DateTime transactionDate,
required BuildContext context,
required dynamic bluetoothService,
PaymentInfo? paymentInfo,
}) async {
print('=== FUNGSI printToThermalPrinter DIPANGGIL ===');
print('Memulai proses pencetakan struk...');
print('Jumlah item: ${items.length}');
print('Tanggal transaksi: $transactionDate');
try {
print(
'Menghasilkan byte array ESC/POS menggunakan flutter_esc_pos_utils...');
// Generate struk dalam format byte array
final bytes = await generateEscPosBytes(
items: items,
transactionDate: transactionDate,
paymentInfo: paymentInfo,
);
print('Byte array ESC/POS berhasil dihasilkan');
print('Jumlah byte: ${bytes.length}');
// Verifikasi koneksi sebelum mencetak
final isConnectedBeforePrint = await bluetoothService.checkConnection();
if (!isConnectedBeforePrint) {
print('Printer tidak terhubung saat akan mencetak');
throw SocketException('Printer tidak terhubung saat akan mencetak');
}
print('Mengirim byte array ke printer...');
try {
// Konversi List<int> ke Uint8List
final Uint8List data = Uint8List.fromList(bytes);
await bluetoothService.printReceipt(data);
print('Perintah cetak berhasil dikirim');
} on SocketException catch (e) {
PrintErrorHandler.logPrintError('SEND_TO_PRINTER', e);
throw SocketException('Koneksi ke printer terputus: ${e.message}');
} on PlatformException catch (e) {
PrintErrorHandler.logPrintError('SEND_TO_PRINTER', e);
throw PlatformException(
code: e.code, message: 'Error printer: ${e.message}');
} catch (printError) {
PrintErrorHandler.logPrintError(
'SEND_TO_PRINTER', printError as Exception);
throw Exception('Gagal mengirim perintah cetak: $printError');
}
} on SocketException {
rethrow; // Lempar ulang error koneksi
} on PlatformException {
rethrow; // Lempar ulang error platform
} catch (e, stackTrace) {
PrintErrorHandler.logPrintError(
'PRINT_THERMAL_PRINTER', e as Exception, stackTrace);
throw Exception('Gagal mencetak struk: $e');
}
}
/// Helper function to get image from file path
static Future<img.Image?> _getImageFromFilePath(String path) async {
try {
final file = File(path);
if (await file.exists()) {
final bytes = await file.readAsBytes();
return img.decodeImage(bytes);
}
} catch (e) {
print('Error getting image from file path: $e');
}
return null;
}
}