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:cashumit/screens/transaction_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:cashumit/screens/receipt_screen.dart';
|
import 'package:cashumit/screens/receipt_screen.dart';
|
||||||
|
import 'package:cashumit/utils/store_logo_utils.dart';
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
void main() {
|
|
||||||
runApp(const MyApp());
|
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