- Changed redirect paths in WithRoleRedirect trait from '/login' to '/auth/login' for consistency. - Adjusted footer layout in home view to include padding for better spacing. - Improved order management UI by modifying flexbox structure for better responsiveness and alignment of buttons.
498 lines
20 KiB
PHP
498 lines
20 KiB
PHP
<flux:main>
|
|
<div class="flex flex-col gap-4 sm:flex-row sm:justify-between sm:items-center">
|
|
<div>
|
|
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
|
</div>
|
|
|
|
@can('create order')
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center" x-data="{
|
|
isConnected: !!(localStorage.getItem('printerDevice') || localStorage.getItem('btPrinterDevice'))
|
|
}"
|
|
@printer-status-change.window="isConnected = $event.detail.connected">
|
|
<flux:button href="{{ route('studio.manage.order.create') }}" variant="primary" wire:navigate
|
|
class="text-sm w-full sm:w-auto">
|
|
Tambah
|
|
</flux:button>
|
|
|
|
<template x-if="!isConnected">
|
|
<div class="flex flex-col gap-2 sm:flex-row">
|
|
<flux:button variant="primary" color="cyan" wire:click="$dispatch('connectUSB')"
|
|
class="w-full sm:w-auto">
|
|
Connect USB
|
|
</flux:button>
|
|
<flux:button variant="primary" color="indigo" wire:click="$dispatch('connectBluetooth')"
|
|
class="w-full sm:w-auto">
|
|
Connect Bluetooth
|
|
</flux:button>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="isConnected">
|
|
<div>
|
|
<flux:button variant="primary" color="red" wire:click="$dispatch('disconnectPrinter')"
|
|
class="w-full sm:w-auto">
|
|
Disconnect
|
|
</flux:button>
|
|
</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;
|
|
window.btCharacteristic = null;
|
|
let currentOrder = null;
|
|
|
|
// Check if device is already stored
|
|
const savedUSB = localStorage.getItem('printerDevice');
|
|
const savedBT = localStorage.getItem('btPrinterDevice');
|
|
if (savedUSB || savedBT) {
|
|
// 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,
|
|
};
|
|
|
|
localStorage.setItem('printerDevice', JSON.stringify(deviceInfo));
|
|
localStorage.removeItem('btPrinterDevice'); // Clear BT if USB is connected
|
|
|
|
console.log('Connected to USB:', device.productName);
|
|
window.dispatchEvent(new CustomEvent('printer-status-change', {
|
|
detail: {
|
|
connected: true
|
|
}
|
|
}));
|
|
|
|
} catch (err) {
|
|
console.error('Gagal connect USB:', err);
|
|
}
|
|
});
|
|
|
|
Livewire.on('disconnectPrinter', async (event) => {
|
|
try {
|
|
// Clear USB connection
|
|
if (window.printerDevice) {
|
|
try {
|
|
await window.printerDevice.close();
|
|
} catch (err) {
|
|
console.log('Error closing USB device:', err);
|
|
}
|
|
window.printerDevice = null;
|
|
}
|
|
|
|
// Clear Bluetooth connection
|
|
if (window.btCharacteristic) {
|
|
try {
|
|
await window.btCharacteristic.service.device.gatt.disconnect();
|
|
} catch (err) {
|
|
console.log('Error disconnecting Bluetooth:', err);
|
|
}
|
|
window.btCharacteristic = null;
|
|
}
|
|
|
|
// Clear localStorage
|
|
localStorage.removeItem('printerDevice');
|
|
localStorage.removeItem('btPrinterDevice');
|
|
|
|
console.log('Printer disconnected');
|
|
window.dispatchEvent(new CustomEvent('printer-status-change', {
|
|
detail: {
|
|
connected: false
|
|
}
|
|
}));
|
|
|
|
} catch (err) {
|
|
console.error('Gagal disconnect printer:', err);
|
|
}
|
|
});
|
|
|
|
Livewire.on('connectBluetooth', async (event) => {
|
|
try {
|
|
const device = await navigator.bluetooth.requestDevice({
|
|
filters: [{
|
|
services: ['000018f0-0000-1000-8000-00805f9b34fb']
|
|
},
|
|
{
|
|
services: ['0000ff00-0000-1000-8000-00805f9b34fb']
|
|
},
|
|
{
|
|
services: ['49535343-fe7d-4ae5-8fa9-9fafd205e455']
|
|
}, // microchip
|
|
{
|
|
namePrefix: 'TP'
|
|
},
|
|
{
|
|
namePrefix: 'RP'
|
|
},
|
|
{
|
|
namePrefix: 'MPT'
|
|
},
|
|
{
|
|
namePrefix: 'InnerPrinter'
|
|
},
|
|
{
|
|
namePrefix: 'Bluetooth'
|
|
}
|
|
],
|
|
optionalServices: [
|
|
'00001101-0000-1000-8000-00805f9b34fb',
|
|
'000018f0-0000-1000-8000-00805f9b34fb',
|
|
'0000ff00-0000-1000-8000-00805f9b34fb',
|
|
'49535343-fe7d-4ae5-8fa9-9fafd205e455'
|
|
]
|
|
});
|
|
|
|
const server = await device.gatt.connect();
|
|
|
|
// Try to find the write characteristic
|
|
const services = await server.getPrimaryServices();
|
|
let characteristic = null;
|
|
|
|
for (const service of services) {
|
|
const characteristics = await service.getCharacteristics();
|
|
for (const char of characteristics) {
|
|
if (char.properties.write || char.properties.writeWithoutResponse) {
|
|
characteristic = char;
|
|
break;
|
|
}
|
|
}
|
|
if (characteristic) break;
|
|
}
|
|
|
|
if (!characteristic) {
|
|
throw new Error("Tidak menemukan characteristic untuk menulis data.");
|
|
}
|
|
|
|
window.btCharacteristic = characteristic;
|
|
|
|
const deviceInfo = {
|
|
name: device.name,
|
|
id: device.id
|
|
};
|
|
|
|
localStorage.setItem('btPrinterDevice', JSON.stringify(deviceInfo));
|
|
localStorage.removeItem('printerDevice'); // Clear USB if BT is connected
|
|
|
|
console.log('Connected to Bluetooth:', device.name);
|
|
window.dispatchEvent(new CustomEvent('printer-status-change', {
|
|
detail: {
|
|
connected: true
|
|
}
|
|
}));
|
|
|
|
device.addEventListener('gattserverdisconnected', () => {
|
|
console.log('Bluetooth disconnected');
|
|
window.btCharacteristic = null;
|
|
localStorage.removeItem('btPrinterDevice');
|
|
window.dispatchEvent(new CustomEvent('printer-status-change', {
|
|
detail: {
|
|
connected: false
|
|
}
|
|
}));
|
|
});
|
|
|
|
} catch (err) {
|
|
console.error('Gagal connect Bluetooth:', err);
|
|
alert('Gagal menghubungkan Bluetooth: ' + err.message);
|
|
}
|
|
});
|
|
|
|
async function autoReconnect() {
|
|
const savedUSB = localStorage.getItem('printerDevice');
|
|
const savedBT = localStorage.getItem('btPrinterDevice');
|
|
|
|
if (savedUSB) {
|
|
const {
|
|
vendorId,
|
|
productId
|
|
} = JSON.parse(savedUSB);
|
|
try {
|
|
const devices = await navigator.usb.getDevices();
|
|
const target = devices.find(d => d.vendorId === vendorId && d.productId === productId);
|
|
|
|
if (target) {
|
|
await target.open();
|
|
if (target.configuration === null) {
|
|
await target.selectConfiguration(1);
|
|
}
|
|
await target.claimInterface(0);
|
|
window.printerDevice = target;
|
|
console.log('Auto reconnected to USB:', target.productName);
|
|
}
|
|
} catch (err) {
|
|
console.error('Gagal autoreconnect USB:', err);
|
|
}
|
|
} else if (savedBT) {
|
|
// Bluetooth doesn't support silent reconnect in many browsers without user gesture
|
|
// But we can check if it's still connected or just clear it if it needs manual action
|
|
console.log('Bluetooth device saved, but may need manual reconnection if disconnected.');
|
|
}
|
|
|
|
const isConnected = !!(window.printerDevice || window.btCharacteristic);
|
|
window.dispatchEvent(new CustomEvent('printer-status-change', {
|
|
detail: {
|
|
connected: isConnected
|
|
}
|
|
}));
|
|
}
|
|
|
|
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 && !window.btCharacteristic) {
|
|
// Try auto reconnect or prompt
|
|
await autoReconnect();
|
|
if (!window.printerDevice && !window.btCharacteristic) {
|
|
alert("Printer belum terkoneksi via USB atau Bluetooth.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
// ESC/POS Commands
|
|
const ESC = "\x1B";
|
|
const reset = new Uint8Array([0x1B, 0x40]);
|
|
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 : ${order.date}\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 DATA FUNCTION
|
|
const sendData = async (data) => {
|
|
if (window.printerDevice) {
|
|
await window.printerDevice.transferOut(1, data);
|
|
} else if (window.btCharacteristic) {
|
|
// Bluetooth often has a limit on packet size (typically 20-512 bytes)
|
|
// Split data into chunks if necessary
|
|
const chunkSize = 100;
|
|
for (let i = 0; i < data.length; i += chunkSize) {
|
|
const chunk = data.slice(i, i + chunkSize);
|
|
await window.btCharacteristic.writeValue(chunk);
|
|
}
|
|
}
|
|
};
|
|
|
|
try {
|
|
await sendData(reset);
|
|
await sendData(textBytes);
|
|
await sendData(cut);
|
|
} catch (err) {
|
|
console.error('Gagal mencetak:', err);
|
|
alert('Gagal mencetak: ' + err.message);
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
@endscript
|