Files
app-grosir/lib/services/printer_service.dart

221 lines
6.5 KiB
Dart

import 'package:esc_pos_utils_plus/esc_pos_utils_plus.dart';
import 'package:print_bluetooth_thermal/print_bluetooth_thermal.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/sale.dart';
import '../utils/format.dart';
class PrinterService {
static const String _printerKey = 'selected_printer_mac';
String? _connectedMac;
bool _isConnected = false;
String? get connectedMac => _connectedMac;
bool get isConnected => _isConnected;
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
_connectedMac = prefs.getString(_printerKey);
if (_connectedMac != null) {
await connect(_connectedMac!);
}
}
Future<bool> isBluetoothEnabled() async {
return PrintBluetoothThermal.bluetoothEnabled;
}
Future<List<BluetoothInfo>> getPairedDevices() async {
final list = await PrintBluetoothThermal.pairedBluetooths;
return list;
}
Future<bool> connect(String mac) async {
try {
final connected = await PrintBluetoothThermal.connect(macPrinterAddress: mac);
_isConnected = connected;
if (connected) {
_connectedMac = mac;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_printerKey, mac);
}
return connected;
} catch (_) {
_isConnected = false;
return false;
}
}
Future<void> disconnect() async {
try {
await PrintBluetoothThermal.disconnect;
} catch (_) {}
_isConnected = false;
}
Future<void> forgetPrinter() async {
_connectedMac = null;
_isConnected = false;
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_printerKey);
}
Future<List<int>> _buildReceiptBytes(Sale sale, String storeName, {String? kasirName, String? whatsapp, String? notes}) async {
final profile = await CapabilityProfile.load();
final generator = Generator(PaperSize.mm58, profile);
final bytes = <int>[];
bytes.addAll(generator.text(
storeName,
styles: const PosStyles(
align: PosAlign.center,
bold: true,
height: PosTextSize.size2,
),
));
bytes.addAll(generator.text(
formatTanggal(sale.date),
styles: const PosStyles(align: PosAlign.center),
));
bytes.addAll(generator.text(
formatWaktu(sale.date),
styles: const PosStyles(align: PosAlign.center),
));
bytes.addAll(generator.text(
'#${sale.id.toUpperCase()}',
styles: const PosStyles(align: PosAlign.center),
));
bytes.addAll(generator.hr());
for (final it in sale.items) {
bytes.addAll(generator.text('${it.name} x${it.qty}',
styles: const PosStyles(bold: true)));
bytes.addAll(generator.row([
PosColumn(
text: formatRupiah(it.price),
width: 6,
),
PosColumn(
text: formatRupiah(it.subtotal),
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]));
}
bytes.addAll(generator.hr());
if (sale.discount > 0) {
bytes.addAll(generator.row([
PosColumn(text: 'Diskon', width: 6),
PosColumn(
text: '-${formatRupiah(sale.discount)}',
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]));
}
bytes.addAll(generator.row([
PosColumn(
text: 'TOTAL',
width: 6,
styles: const PosStyles(bold: true),
),
PosColumn(
text: formatRupiah(sale.total),
width: 6,
styles: const PosStyles(bold: true, align: PosAlign.right),
),
]));
final methodLabel = sale.paymentMethod == 'transfer'
? 'Bank'
: sale.paymentMethod == 'qris'
? 'QRIS'
: sale.paymentMethod == 'split'
? 'Split Payment'
: 'Tunai';
bytes.addAll(generator.row([
PosColumn(text: 'Metode', width: 6),
PosColumn(
text: methodLabel,
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]));
if (sale.paymentMethod == 'tunai') {
bytes.addAll(generator.row([
PosColumn(text: 'Bayar', width: 6),
PosColumn(
text: formatRupiah(sale.paid),
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]));
bytes.addAll(generator.row([
PosColumn(text: 'Kembali', width: 6),
PosColumn(
text: formatRupiah(sale.change),
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]));
}
if (sale.splitPayment != null && sale.splitPayment!.isNotEmpty) {
for (final entry in sale.splitPayment!.entries) {
final label = entry.key == 'tunai'
? 'Tunai'
: entry.key == 'transfer'
? 'Transfer'
: 'QRIS';
bytes.addAll(generator.row([
PosColumn(text: 'Split $label', width: 6),
PosColumn(
text: formatRupiah(entry.value),
width: 6,
styles: const PosStyles(align: PosAlign.right),
),
]));
}
}
if (kasirName != null && kasirName.isNotEmpty) {
bytes.addAll(generator.text('Kasir: $kasirName',
styles: const PosStyles(align: PosAlign.center)));
}
if (whatsapp != null && whatsapp.isNotEmpty) {
bytes.addAll(generator.text('WA: $whatsapp',
styles: const PosStyles(align: PosAlign.center)));
}
if (notes != null && notes.trim().isNotEmpty) {
final noteLines = notes.trim().split('\n');
for (final line in noteLines) {
final text = line.trim();
if (text.isEmpty) continue;
bytes.addAll(generator.text(text,
styles: const PosStyles(align: PosAlign.center)));
}
}
bytes.addAll(generator.text(
'Terima kasih!',
styles: const PosStyles(align: PosAlign.center),
));
bytes.addAll(generator.cut());
return bytes;
}
Future<String?> printSale(Sale sale, String storeName, {String? kasirName, String? whatsapp, String? notes}) async {
if (!_isConnected || _connectedMac == null) {
return 'Printer belum terhubung';
}
try {
final bytes = await _buildReceiptBytes(sale, storeName, kasirName: kasirName, whatsapp: whatsapp, notes: notes);
final result = await PrintBluetoothThermal.writeBytes(bytes);
if (result) return null;
return 'Gagal mengirim ke printer';
} catch (e) {
return e.toString();
}
}
}
final printerService = PrinterService();