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
							parent
							
								
									06b23d61db
								
							
						
					
					
						commit
						b848881805
					
				| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1 @@
 | 
			
		|||
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==
 | 
			
		||||
| 
						 | 
				
			
			@ -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());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<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';
 | 
			
		||||
    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<int> 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<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));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<void> 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<void> removeStoreLogoPath() async {
 | 
			
		||||
  final prefs = await SharedPreferences.getInstance();
 | 
			
		||||
  await prefs.remove('store_logo_path');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Fungsi untuk mengambil path logo toko dari shared preferences
 | 
			
		||||
Future<String?> 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<void> 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue