390 lines
14 KiB
Dart
390 lines
14 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';
|
|
|
|
/// 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,
|
|
required String cashierId,
|
|
required String transactionId,
|
|
}) async {
|
|
print('Memulai generateEscPosBytes...');
|
|
print('Jumlah item: ${items.length}');
|
|
print('Tanggal transaksi: $transactionDate');
|
|
print('ID kasir: $cashierId');
|
|
print('ID transaksi: $transactionId');
|
|
|
|
// 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<int> 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));
|
|
|
|
// Informasi transaksi
|
|
bytes += generator.text('Kasir: $cashierId',
|
|
styles: PosStyles(bold: true));
|
|
bytes += generator.text('ID Transaksi: $transactionId',
|
|
styles: PosStyles());
|
|
|
|
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<String> 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<void> printToThermalPrinter({
|
|
required List<ReceiptItem> items,
|
|
required DateTime transactionDate,
|
|
required BuildContext context,
|
|
required dynamic bluetoothService, // Kita akan sesuaikan tipe ini nanti
|
|
}) async {
|
|
// Definisikan cashierId dan transactionId di sini karena tidak berubah
|
|
final String cashierId = 'KSR001';
|
|
final String transactionId = 'TXN202508200001';
|
|
|
|
print('=== FUNGSI printToThermalPrinter DIPANGGIL ===');
|
|
print('Memulai proses pencetakan struk...');
|
|
print('Jumlah item: ${items.length}');
|
|
print('Tanggal transaksi: ${transactionDate}');
|
|
print('ID kasir: $cashierId');
|
|
print('ID transaksi: $transactionId');
|
|
|
|
try {
|
|
print('Menghasilkan byte array ESC/POS menggunakan flutter_esc_pos_utils...');
|
|
print('Jumlah item: ${items.length}');
|
|
print('Tanggal transaksi: ${transactionDate}');
|
|
print('ID kasir: $cashierId');
|
|
print('ID transaksi: $transactionId');
|
|
|
|
// Generate struk dalam format byte array menggunakan EscPosPrintService
|
|
final bytes = await generateEscPosBytes(
|
|
items: items,
|
|
transactionDate: transactionDate,
|
|
cashierId: cashierId,
|
|
transactionId: transactionId,
|
|
);
|
|
|
|
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<int> 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<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;
|
|
}
|
|
}
|