From b848881805061c8b646bb0f8c959dccf8266f6ed Mon Sep 17 00:00:00 2001 From: a2nr Date: Sat, 23 Aug 2025 19:47:43 +0700 Subject: [PATCH] Implement logo printing functionality\n\n- Add store_logo_utils.dart with functions to manage logo path\n- Update main.dart to initialize logo from assets\n- Add placeholder logo image in assets/images/store_logo.png\n- Update esc_pos_print_service.dart to include logo in receipt\n- Add INSTRUCTIONS_LOGO.md with instructions for adding logo --- INSTRUCTIONS_LOGO.md | 23 +++ assets/images/store_logo.png | 1 + lib/main.dart | 9 +- lib/services/esc_pos_print_service.dart | 247 ++++++++++++++++++++++++ lib/utils/store_logo_utils.dart | 53 +++++ 5 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 INSTRUCTIONS_LOGO.md create mode 100644 assets/images/store_logo.png create mode 100644 lib/services/esc_pos_print_service.dart create mode 100644 lib/utils/store_logo_utils.dart diff --git a/INSTRUCTIONS_LOGO.md b/INSTRUCTIONS_LOGO.md new file mode 100644 index 0000000..92459e7 --- /dev/null +++ b/INSTRUCTIONS_LOGO.md @@ -0,0 +1,23 @@ +# Instruksi untuk menambahkan logo toko ke aplikasi: + +1. Tambahkan file gambar logo ke folder `assets/images/` dengan nama `store_logo.png` + +2. Tambahkan path ke file `pubspec.yaml` agar file gambar tersebut tersedia di aplikasi: + ```yaml + flutter: + assets: + - assets/images/ + ``` + +3. Untuk menyimpan path logo ke shared preferences, panggil fungsi: + ```dart + import 'package:cashumit/utils/store_logo_utils.dart'; + + // Simpan path logo + await saveStoreLogoPath('assets/images/store_logo.png'); + + // Hapus path logo jika diperlukan + await removeStoreLogoPath(); + ``` + +4. Untuk menguji pencetakan logo, pastikan printer thermal mendukung pencetakan gambar. \ No newline at end of file diff --git a/assets/images/store_logo.png b/assets/images/store_logo.png new file mode 100644 index 0000000..5371d4a --- /dev/null +++ b/assets/images/store_logo.png @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg== \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index a373387..6a56ea0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,8 +2,15 @@ import 'package:cashumit/screens/config_screen.dart'; import 'package:cashumit/screens/transaction_screen.dart'; import 'package:flutter/material.dart'; import 'package:cashumit/screens/receipt_screen.dart'; +import 'package:cashumit/utils/store_logo_utils.dart'; -void main() { +void main() async { + // Ensure WidgetsFlutterBinding is initialized for async operations + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize the store logo from asset + await copyAndSaveStoreLogoFromAsset('assets/images/store_logo.png'); + runApp(const MyApp()); } diff --git a/lib/services/esc_pos_print_service.dart b/lib/services/esc_pos_print_service.dart new file mode 100644 index 0000000..2ca70e3 --- /dev/null +++ b/lib/services/esc_pos_print_service.dart @@ -0,0 +1,247 @@ +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'; + +/// 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, + 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'; + final logoPath = prefs.getString('store_logo_path'); // Load logo path + + print('Nama toko: $storeName'); + print('Alamat toko: $storeAddress'); + print('Nama admin: $adminName'); + print('Telepon admin: $adminPhone'); + print('Logo path: $logoPath'); + + // 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 + final profile = await CapabilityProfile.load(); + final generator = Generator(PaperSize.mm58, profile); + + // Mulai dengan inisialisasi printer + List bytes = []; + + // Tambahkan logo jika ada path-nya + if (logoPath != null && logoPath.isNotEmpty) { + try { + // Membaca file gambar dari path lokal + final file = File(logoPath); + if (await file.exists()) { + final Uint8List imageBytes = await file.readAsBytes(); + + // Decode gambar (format yang didukung: JPEG, PNG) + final img.Image? image = img.decodeImage(imageBytes); + if (image != null) { + bytes += generator.image(image); + } + } else { + throw Exception('File logo tidak ditemukan'); + } + } catch (e) { + print('Gagal memuat logo: $e'); + // Jika gagal memuat logo, gunakan teks sebagai fallback + bytes += generator.text(storeName, + styles: PosStyles( + bold: true, + height: PosTextSize.size1, + width: PosTextSize.size1, + align: PosAlign.center, + )); + } + } else { + // Jika tidak ada logo, gunakan nama toko sebagai header + bytes += generator.text(storeName, + styles: PosStyles( + bold: true, + height: PosTextSize.size1, + width: PosTextSize.size1, + align: PosAlign.center, + )); + } + + 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 + print('Memulai iterasi item...'); + for (int i = 0; i < items.length; i++) { + 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), + ), + ]); + } + print('Selesai iterasi item'); + + // Garis pemisah sebelum total + bytes += generator.text('--------------------------------', + styles: PosStyles(align: PosAlign.center)); + + // Total + final totalAmount = items.fold(0.0, (sum, item) => sum + item.total); + 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 + final customDisclaimer = prefs.getString('store_disclaimer_text') ?? + 'Barang yang sudah dibeli tidak dapat dikembalikan/ditukar. ' + 'Harap periksa kembali struk belanja Anda sebelum meninggalkan toko.'; + final customThankYou = prefs.getString('thank_you_text') ?? '*** TERIMA KASIH ***'; + final 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.'; + + // Menambahkan disclaimer + bytes += generator.feed(1); + + // Memecah disclaimer menjadi beberapa baris jika terlalu panjang + 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)); + } + + // Menambahkan ucapan terima kasih + bytes += generator.feed(1); + bytes += generator.text(customThankYou, + styles: PosStyles(align: PosAlign.center, bold: true)); + + // Menambahkan pantun + bytes += generator.feed(1); + final pantunLines = customPantun.split('\n'); + for (final line in pantunLines) { + bytes += generator.text(line, + styles: PosStyles(align: PosAlign.center)); + } + + // Spasi di akhir dan pemotongan kertas + bytes += generator.feed(2); + bytes += generator.cut(); + + print('Jumlah byte yang dihasilkan: ${bytes.length}'); + + return bytes; + } +} \ No newline at end of file diff --git a/lib/utils/store_logo_utils.dart b/lib/utils/store_logo_utils.dart new file mode 100644 index 0000000..661c797 --- /dev/null +++ b/lib/utils/store_logo_utils.dart @@ -0,0 +1,53 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:path_provider/path_provider.dart'; +import 'dart:io'; +import 'package:flutter/services.dart' show rootBundle; + +/// Fungsi untuk menyimpan path logo toko ke shared preferences +Future saveStoreLogoPath(String logoPath) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('store_logo_path', logoPath); +} + +/// Fungsi untuk menghapus path logo toko dari shared preferences +Future removeStoreLogoPath() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('store_logo_path'); +} + +/// Fungsi untuk mengambil path logo toko dari shared preferences +Future getStoreLogoPath() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('store_logo_path'); +} + +/// Fungsi untuk menyalin logo dari asset ke direktori dokumen aplikasi +/// dan menyimpan path-nya ke shared preferences +Future copyAndSaveStoreLogoFromAsset(String assetPath) async { + try { + // Dapatkan direktori dokumen aplikasi + final dir = await getApplicationDocumentsDirectory(); + final logoDir = Directory('${dir.path}/logos'); + + // Buat direktori jika belum ada + if (!await logoDir.exists()) { + await logoDir.create(recursive: true); + } + + // Path file logo di direktori dokumen + final logoFile = File('${logoDir.path}/store_logo.png'); + + // Baca data dari asset + final data = await rootBundle.load(assetPath); + + // Tulis data ke file di direktori dokumen + await logoFile.writeAsBytes(data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes)); + + // Simpan path ke shared preferences + await saveStoreLogoPath(logoFile.path); + } catch (e) { + print('Error copying logo from asset: $e'); + // Jika gagal, hapus path yang mungkin tersimpan + await removeStoreLogoPath(); + } +} \ No newline at end of file