import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart'; import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/sale.dart'; import '../utils/format.dart'; class PrinterService { static const String _printerKey = 'selected_printer_mac'; String? _connectedMac; bool _isConnected = false; String? get connectedMac => _connectedMac; bool get isConnected => _isConnected; Future init() async { final prefs = await SharedPreferences.getInstance(); _connectedMac = prefs.getString(_printerKey); if (_connectedMac != null) { await connect(_connectedMac!); } } Future isBluetoothEnabled() async { return PrintBluetoothThermal.bluetoothEnabled; } Future> getPairedDevices() async { final list = await PrintBluetoothThermal.pairedBluetooths; return list; } Future connect(String mac) async { try { final connected = await PrintBluetoothThermal.connect(macPrinterAddress: mac); _isConnected = connected; if (connected) { _connectedMac = mac; final prefs = await SharedPreferences.getInstance(); await prefs.setString(_printerKey, mac); } return connected; } catch (_) { _isConnected = false; return false; } } Future disconnect() async { try { await PrintBluetoothThermal.disconnect; } catch (_) {} _isConnected = false; } Future forgetPrinter() async { _connectedMac = null; _isConnected = false; final prefs = await SharedPreferences.getInstance(); await prefs.remove(_printerKey); } Future> _buildReceiptBytes(Sale sale, String storeName, {String? kasirName, String? whatsapp, String? notes}) async { final profile = await CapabilityProfile.load(); final generator = Generator(PaperSize.mm58, profile); final bytes = []; bytes.addAll(generator.text( storeName, styles: const PosStyles( align: PosAlign.center, bold: true, height: PosTextSize.size2, ), )); bytes.addAll(generator.text( formatTanggal(sale.date), styles: const PosStyles(align: PosAlign.center), )); bytes.addAll(generator.text( formatWaktu(sale.date), styles: const PosStyles(align: PosAlign.center), )); bytes.addAll(generator.text( '#${sale.id.toUpperCase()}', styles: const PosStyles(align: PosAlign.center), )); bytes.addAll(generator.hr()); for (final it in sale.items) { bytes.addAll(generator.text('${it.name} x${it.qty}', styles: const PosStyles(bold: true))); bytes.addAll(generator.row([ PosColumn( text: formatRupiah(it.price), width: 6, ), PosColumn( text: formatRupiah(it.subtotal), width: 6, styles: const PosStyles(align: PosAlign.right), ), ])); } bytes.addAll(generator.hr()); if (sale.discount > 0) { bytes.addAll(generator.row([ PosColumn(text: 'Diskon', width: 6), PosColumn( text: '-${formatRupiah(sale.discount)}', width: 6, styles: const PosStyles(align: PosAlign.right), ), ])); } bytes.addAll(generator.row([ PosColumn( text: 'TOTAL', width: 6, styles: const PosStyles(bold: true), ), PosColumn( text: formatRupiah(sale.total), width: 6, styles: const PosStyles(bold: true, align: PosAlign.right), ), ])); final methodLabel = sale.paymentMethod == 'transfer' ? 'Bank' : sale.paymentMethod == 'qris' ? 'QRIS' : sale.paymentMethod == 'split' ? 'Split Payment' : 'Tunai'; bytes.addAll(generator.row([ PosColumn(text: 'Metode', width: 6), PosColumn( text: methodLabel, width: 6, styles: const PosStyles(align: PosAlign.right), ), ])); if (sale.paymentMethod == 'tunai') { bytes.addAll(generator.row([ PosColumn(text: 'Bayar', width: 6), PosColumn( text: formatRupiah(sale.paid), width: 6, styles: const PosStyles(align: PosAlign.right), ), ])); bytes.addAll(generator.row([ PosColumn(text: 'Kembali', width: 6), PosColumn( text: formatRupiah(sale.change), width: 6, styles: const PosStyles(align: PosAlign.right), ), ])); } if (sale.splitPayment != null && sale.splitPayment!.isNotEmpty) { for (final entry in sale.splitPayment!.entries) { final label = entry.key == 'tunai' ? 'Tunai' : entry.key == 'transfer' ? 'Transfer' : 'QRIS'; bytes.addAll(generator.row([ PosColumn(text: 'Split $label', width: 6), PosColumn( text: formatRupiah(entry.value), width: 6, styles: const PosStyles(align: PosAlign.right), ), ])); } } if (kasirName != null && kasirName.isNotEmpty) { bytes.addAll(generator.text('Kasir: $kasirName', styles: const PosStyles(align: PosAlign.center))); } if (whatsapp != null && whatsapp.isNotEmpty) { bytes.addAll(generator.text('WA: $whatsapp', styles: const PosStyles(align: PosAlign.center))); } if (notes != null && notes.trim().isNotEmpty) { final noteLines = notes.trim().split('\n'); for (final line in noteLines) { final text = line.trim(); if (text.isEmpty) continue; bytes.addAll(generator.text(text, styles: const PosStyles(align: PosAlign.center))); } } bytes.addAll(generator.text( 'Terima kasih!', styles: const PosStyles(align: PosAlign.center), )); bytes.addAll(generator.cut()); return bytes; } Future printSale(Sale sale, String storeName, {String? kasirName, String? whatsapp, String? notes}) async { if (!_isConnected || _connectedMac == null) { return 'Printer belum terhubung'; } try { final bytes = await _buildReceiptBytes(sale, storeName, kasirName: kasirName, whatsapp: whatsapp, notes: notes); final result = await PrintBluetoothThermal.writeBytes(bytes); if (result) return null; return 'Gagal mengirim ke printer'; } catch (e) { return e.toString(); } } } final printerService = PrinterService();