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'; /// Service untuk menghasilkan perintah ESC/POS menggunakan flutter_esc_pos_utils class EscPosPrintService { /// Menghasilkan struk dalam format byte array berdasarkan data transaksi static Future> generateEscPosBytes({ required List items, required DateTime transactionDate, }) async { print('Memulai generateEscPosBytes...'); print('Jumlah item: ${items.length}'); print('Tanggal transaksi: $transactionDate'); // Load store info from shared preferences final prefs = await SharedPreferences.getInstance(); 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'; print('Nama toko: $storeName'); print('Alamat toko: $storeAddress'); print('Nama admin: $adminName'); print('Telepon admin: $adminPhone'); // Format tanggal final dateFormatter = DateFormat('dd/MM/yyyy HH:mm'); final formattedDate = dateFormatter.format(transactionDate); print('Tanggal yang diformat: $formattedDate'); // Format angka ke rupiah String formatRupiah(double amount) { final formatter = NumberFormat("#,##0", "id_ID"); return "Rp ${formatter.format(amount)}"; } // Load capability profile with timeout CapabilityProfile profile; try { profile = await CapabilityProfile.load(); } catch (e) { print('Gagal memuat capability profile: $e'); // Gunakan profile default jika gagal profile = await CapabilityProfile.load(); } final generator = Generator(PaperSize.mm58, profile); // Mulai dengan inisialisasi printer List bytes = []; // Coba tambahkan logo toko jika ada final logoPath = prefs.getString('store_logo_path'); 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 += generator.image(resizedLogo, align: PosAlign.center); bytes += generator.feed(1); // Beri sedikit spasi setelah logo } } catch (e) { print('Error loading or processing store logo: $e'); } } // Tambahkan nama toko sebagai header bytes += generator.text(storeName, styles: PosStyles( bold: true, height: PosTextSize.size1, width: PosTextSize.size1, align: PosAlign.center, )); try { bytes += generator.text(storeAddress, styles: PosStyles(align: PosAlign.center)); bytes += generator.text('Admin: $adminName', styles: PosStyles(align: PosAlign.center)); bytes += generator.text('Telp: $adminPhone', styles: PosStyles(align: PosAlign.center)); bytes += generator.text('$formattedDate', styles: PosStyles(align: PosAlign.center)); bytes += generator.feed(1); // Garis pemisah bytes += generator.text('================================', styles: PosStyles(align: PosAlign.center)); // Tabel item bytes += 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), ), ]); // Item list dengan penanganan error 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}'); // Untuk item dengan detail kuantitas dan harga bytes += generator.row([ PosColumn( text: item.description, width: 12, styles: PosStyles(align: PosAlign.left), ), ]); bytes += 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'); // Garis pemisah sebelum total bytes += generator.text('--------------------------------', styles: PosStyles(align: PosAlign.center)); // Total double totalAmount = 0.0; try { totalAmount = items.fold(0.0, (sum, item) => sum + item.total); } catch (e) { print('Error saat menghitung total: $e'); totalAmount = 0.0; } print('Total amount: $totalAmount'); bytes += 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), ), ]); // Garis pemisah setelah total bytes += generator.text('================================', styles: PosStyles(align: PosAlign.center)); // Memuat teks kustom dari shared preferences String customDisclaimer; try { customDisclaimer = prefs.getString('store_disclaimer_text') ?? 'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. ' 'Harap periksa kembali struk belanja Anda sebelum meninggalkan toko.'; } catch (e) { print('Error saat memuat disclaimer: $e'); customDisclaimer = 'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar.'; } String customThankYou; try { customThankYou = prefs.getString('thank_you_text') ?? '*** TERIMA KASIH ***'; } catch (e) { print('Error saat memuat thank you text: $e'); customThankYou = '*** TERIMA KASIH ***'; } String customPantun; try { customPantun = 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.'; } catch (e) { print('Error saat memuat pantun: $e'); customPantun = ''; } // Menambahkan disclaimer bytes += generator.feed(1); // Memecah disclaimer menjadi beberapa baris jika terlalu panjang try { final disclaimerLines = customDisclaimer.split(' '); final List wrappedDisclaimer = []; String currentLine = ''; for (final word in disclaimerLines) { if ((currentLine + word).length > 32) { wrappedDisclaimer.add(currentLine.trim()); currentLine = word + ' '; } else { currentLine += word + ' '; } } if (currentLine.trim().isNotEmpty) { wrappedDisclaimer.add(currentLine.trim()); } for (final line in wrappedDisclaimer) { bytes += generator.text(line, styles: PosStyles(align: PosAlign.center)); } } catch (e) { print('Error saat memproses disclaimer: $e'); // Fallback jika ada error bytes += generator.text(customDisclaimer, styles: PosStyles(align: PosAlign.center)); } // Menambahkan ucapan terima kasih bytes += generator.feed(1); bytes += generator.text(customThankYou, styles: PosStyles(align: PosAlign.center, bold: true)); // Menambahkan pantun jika ada if (customPantun.isNotEmpty) { try { bytes += generator.feed(1); final pantunLines = customPantun.split('\n'); for (final line in pantunLines) { bytes += generator.text(line, styles: PosStyles(align: PosAlign.center)); } } catch (e) { print('Error saat menambahkan pantun: $e'); } } // Spasi di akhir dan pemotongan kertas bytes += generator.feed(2); bytes += generator.cut(); } catch (e) { print('Error saat membuat bagian akhir struk: $e'); // Pastikan struk tetap dipotong meski ada error bytes += generator.feed(2); bytes += generator.cut(); } print('Jumlah byte yang dihasilkan: ${bytes.length}'); return bytes; } /// Mencetak struk ke printer thermal static Future printToThermalPrinter({ required List items, required DateTime transactionDate, required BuildContext context, required dynamic bluetoothService, // Kita akan sesuaikan tipe ini nanti }) 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...'); print('Jumlah item: ${items.length}'); print('Tanggal transaksi: ${transactionDate}'); // Generate struk dalam format byte array menggunakan EscPosPrintService final bytes = await generateEscPosBytes( items: items, transactionDate: transactionDate, ); print('Byte array ESC/POS berhasil dihasilkan'); print('Jumlah byte: ${bytes.length}'); // Tampilkan byte array untuk debugging (dalam format hex) print('Isi byte array (hex):'); if (bytes.length <= 1000) { // Batasi tampilan untuk mencegah output terlalu panjang print(bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ')); } else { print('Terlalu banyak byte untuk ditampilkan (${bytes.length} bytes)'); } // 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 ke Uint8List final Uint8List data = Uint8List.fromList(bytes); await bluetoothService.printReceipt(data); print('Perintah cetak berhasil dikirim'); } on SocketException catch (e) { print('Socket error saat mengirim perintah cetak ke printer: $e'); throw SocketException('Koneksi ke printer terputus: ${e.message}'); } on PlatformException catch (e) { print('Platform error saat mengirim perintah cetak ke printer: $e'); throw PlatformException( code: e.code, message: 'Error printer: ${e.message}'); } catch (printError) { print('Error saat mengirim perintah cetak ke printer: $printError'); throw Exception('Gagal mengirim perintah cetak: $printError'); } } on SocketException { rethrow; // Lempar ulang error koneksi } on PlatformException { rethrow; // Lempar ulang error platform } catch (e, stackTrace) { print('Error saat mencetak struk: $e'); print('Stack trace: $stackTrace'); // Cetak detail error tambahan print('Tipe error: ${e.runtimeType}'); throw Exception('Gagal mencetak struk: $e'); } } /// Helper function to get image from file path static Future 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; } }