parfum/resources/views/livewire/studio/manage/order/index.blade.php

342 lines
13 KiB
PHP

<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
@can('create order')
<div class="flex" x-data="{ isConnected: !!localStorage.getItem('printerDevice') }" @printer-status-change.window="isConnected = $event.detail.connected">
<flux:button href="{{ route('studio.manage.order.create') }}" variant="primary" wire:navigate class="text-sm">
Tambah
</flux:button>
<template x-if="!isConnected">
<flux:button variant="primary" color="cyan" class="ms-2" wire:click="$dispatch('connectUSB')">
Connect USB
</flux:button>
</template>
<template x-if="isConnected">
<div class="ms-2 flex items-center gap-2 text-green-600 bg-green-50 px-3 py-1 rounded-lg border border-green-200">
<flux:icon.check-circle class="size-4" />
<span class="text-sm font-medium">Printer Connected</span>
</div>
</template>
</div>
@endcan
</div>
<div class="mt-6">
<livewire:datatable.manage.orders-table />
</div>
@include('components.modals.confirmation', [
'modalName' => 'delete-confirmation',
'modalTitle' => 'Apakah Anda yakin?',
'modalMessage' => 'Data yang berelasi dengan data ini juga akan ikut terhapus.',
'buttonVariant' => 'primary',
'buttonColor' => 'danger',
'buttonText' => 'Ya, Hapus',
])
{{-- Print Modal --}}
<flux:modal.trigger name="print-modal">
<button id="btn-trigger-print-modal" class="hidden"></button>
</flux:modal.trigger>
<flux:modal name="print-modal" class="w-full max-w-sm">
<div class="space-y-6" x-data="{ paperSize: '58' }">
<div>
<flux:heading size="lg">Cetak Struk</flux:heading>
<flux:subheading>Pilih ukuran kertas printer thermal Anda.</flux:subheading>
</div>
<flux:radio.group label="Ukuran Kertas" x-model="paperSize">
<flux:radio value="58" label="58mm" />
<flux:radio value="80" label="80mm" />
</flux:radio.group>
<div class="flex justify-end gap-2">
<flux:button variant="ghost" x-on:click="$dispatch('modal-close')">Batal</flux:button>
<flux:button variant="primary"
x-on:click="$dispatch('trigger-print-process', { paperSize: paperSize })">
Cetak
</flux:button>
</div>
</div>
</flux:modal>
</flux:main>
@script
<script>
document.addEventListener('livewire:navigated', () => {
window.printerDevice = null;
let currentOrder = null;
// Check if device is already stored
const savedDevice = localStorage.getItem('printerDevice');
if (savedDevice) {
// Dispatch event to update UI
setTimeout(() => window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: true
}
})), 100);
}
Livewire.on('connectUSB', async (event) => {
try {
const device = await navigator.usb.requestDevice({
filters: [{}]
});
await device.open();
if (device.configuration === null) {
await device.selectConfiguration(1);
}
await device.claimInterface(0);
window.printerDevice = device;
const deviceInfo = {
vendorId: device.vendorId,
productId: device.productId,
productName: device.productName,
manufacturerName: device.manufacturerName,
serialNumber: device.serialNumber
};
localStorage.setItem('printerDevice', JSON.stringify(deviceInfo));
console.log('Connected to:', device.productName);
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: true
}
}));
} catch (err) {
console.error('Gagal connect:', err);
}
});
async function autoReconnect() {
const saved = localStorage.getItem('printerDevice');
if (!saved) return;
const {
vendorId,
productId
} = JSON.parse(saved);
const devices = await navigator.usb.getDevices();
const target = devices.find(d => d.vendorId === vendorId && d.productId === productId);
if (!target) return;
try {
await target.open();
if (target.configuration === null) {
await target.selectConfiguration(1);
}
await target.claimInterface(0);
window.printerDevice = target;
console.log('Auto reconnected to:', target.productName);
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: true
}
}));
} catch (err) {
console.error('Gagal autoreconnect:', err);
// If fail, remove storage
localStorage.removeItem('printerDevice');
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: false
}
}));
}
}
autoReconnect();
navigator.usb.addEventListener('disconnect', event => {
const device = event.device;
const saved = localStorage.getItem('printerDevice');
if (!saved) return;
const {
vendorId,
productId
} = JSON.parse(saved);
if (device.vendorId === vendorId && device.productId === productId) {
localStorage.removeItem('printerDevice');
window.printerDevice = null;
console.log('USB dicabut, localStorage dibersihkan.');
window.dispatchEvent(new CustomEvent('printer-status-change', {
detail: {
connected: false
}
}));
}
});
function getMaxChar(widthMM) {
// 58mm uses approx 32 chars (Font A)
// 80mm uses approx 42-48 chars (Font A)
if (parseInt(widthMM) === 58) return 32;
return 48; // for 80mm
}
Livewire.on('open-print-modal', (data) => {
currentOrder = data; // Store data
// Trigger modal open
const btn = document.getElementById('btn-trigger-print-modal');
if (btn) btn.click();
});
window.addEventListener('trigger-print-process', async (e) => {
if (!currentOrder) return;
const paperSize = e.detail.paperSize;
// Close modal
// Assuming Flux handles close on click or we dispatch close
// We will try to close it via dispatching 'close-modal' if configured, but Flux
// buttons inside modal might need `data-flux-close` or similar?
// The provided code used $dispatch('modal-close') which might be custom.
// We will proceed to print.
// Ideally we close the modal:
// document.dispatchEvent(new CustomEvent('close-modal', { detail: 'print-modal' })); // Hypothetical
await printReceipt(currentOrder, parseInt(paperSize));
});
async function printReceipt(order, paperSize = 58) {
if (!window.printerDevice) {
// Try auto reconnect if not connected but saved
await autoReconnect();
if (!window.printerDevice) {
alert("Printer belum terkoneksi via USB.");
return;
}
}
const encoder = new TextEncoder();
const device = window.printerDevice;
// ESC/POS Commands
const ESC = "\x1B";
const GS = "\x1D";
const reset = new Uint8Array([0x1B, 0x40]);
const alignLeft = new Uint8Array([0x1B, 0x61, 0]);
const alignCenter = new Uint8Array([0x1B, 0x61, 1]);
const alignRight = new Uint8Array([0x1B, 0x61, 2]);
const cut = new Uint8Array([0x1D, 0x56, 0x00]);
const MAX = getMaxChar(paperSize);
const line = "-".repeat(MAX) + "\n";
// Function to format text rows
const formatRow = (left, right, maxLen) => {
let out = "";
const leftLen = left.length;
const rightLen = right.length;
const space = maxLen - leftLen - rightLen;
if (space >= 0) {
out += left + " ".repeat(space) + right + "\n";
} else {
// Wrap
out += left + "\n";
out += " ".repeat(maxLen - rightLen) + right + "\n";
}
return out;
};
// BUILD TEXT
let text = "";
// HEADER
text += ESC + "\x61\x01"; // Center
// text += ESC + "\x21\x30"; // Double Height Width logic? 0x30 = 00110000 -> bit 4,5 set.
// Standard thermal printer double size:
text += ESC + "!" + "\x10"; // Double Height
text += "Yadi Parfum\n";
text += ESC + "!" + "\x00"; // Normal
if (order.outlet_address) {
text += order.outlet_address + "\n";
}
text += "\n";
text += ESC + "\x61\x00"; // Left Align
text += `Kasir : ${order.cashier || '-'}\n`;
text += `Customer : ${order.customer || '-'}\n`;
text += `Tanggal : ${new Date().toLocaleString('id-ID')}\n`;
text += line;
// CONTENT
order.items.forEach((i, idx) => {
const isLast = idx === order.items.length - 1;
// Item Name
text += i.name + "\n";
// Quality or Quantity logic
// If quality is present and not 'Custom', show Quality
// Else show numeric math
const qualityName = (i.quality && i.quality !== 'Custom') ? i.quality : null;
if (qualityName) {
// Display: QualityName ......... TotalPrice
text += formatRow(qualityName, Number(i.total).toLocaleString('id-ID'), MAX);
} else {
// Display: 2 x 10,000 ......... 20,000
// i.quantity might be formatted string in some contexts, but here it's likely number from PHP
const qtyStr =
`${i.quantity} x ${Number(i.unit_price).toLocaleString('id-ID')}`;
text += formatRow(qtyStr, Number(i.total).toLocaleString('id-ID'), MAX);
}
// Add extra newline if not packed tight
text += "\n";
});
text += line;
// FOOTER
text += formatRow("Subtotal", Number(order.subtotal).toLocaleString('id-ID'), MAX);
if (order.discount > 0) {
text += formatRow("Diskon", "-" + Number(order.discount).toLocaleString('id-ID'), MAX);
}
text += "\n";
text += ESC + "\x61\x01"; // Center
text += ESC + "\x45\x01"; // Bold On
text += `TOTAL: ${Number(order.total).toLocaleString('id-ID')}\n`;
text += ESC + "\x45\x00"; // Bold Off
text += line;
text += "Terima kasih telah berbelanja\n";
text += "dan memilih Yadi Parfum :)\n";
// Add some feed
text += "\n\n\n";
const textBytes = encoder.encode(text);
// SEND
await device.transferOut(1, reset);
await device.transferOut(1, textBytes);
await device.transferOut(1, cut);
}
});
</script>
@endscript