feat(order): membuat tombol print di datatable
- membuat fitur print - after create bisa pangsung print
This commit is contained in:
parent
ae8a5e4ce8
commit
44697030f8
@ -114,6 +114,12 @@ public function columns(): array
|
|||||||
->label(function ($row) {
|
->label(function ($row) {
|
||||||
$actions = '';
|
$actions = '';
|
||||||
|
|
||||||
|
if (auth()->user()->can('create order') && auth()->user()->can('show order')) {
|
||||||
|
$actions .= view('components.datatables.print', [
|
||||||
|
'id' => $row->hash,
|
||||||
|
])->render();
|
||||||
|
}
|
||||||
|
|
||||||
if (auth()->user()->can('show order')) {
|
if (auth()->user()->can('show order')) {
|
||||||
$actions .= view('components.datatables.show', [
|
$actions .= view('components.datatables.show', [
|
||||||
'id' => $row->hash,
|
'id' => $row->hash,
|
||||||
|
|||||||
@ -105,7 +105,9 @@ public function save()
|
|||||||
|
|
||||||
$this->toast('Order berhasil ditambahkan.');
|
$this->toast('Order berhasil ditambahkan.');
|
||||||
|
|
||||||
$this->redirectRoute('studio.manage.order.index');
|
$this->redirectRoute('studio.manage.order.index', [
|
||||||
|
'order' => $result['data']->id,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function updatedFormOutletId($value)
|
public function updatedFormOutletId($value)
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
use App\Traits\WithConfirmation;
|
use App\Traits\WithConfirmation;
|
||||||
use App\Traits\WithToast;
|
use App\Traits\WithToast;
|
||||||
use Flux\Flux;
|
use Flux\Flux;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
@ -17,6 +18,25 @@ class Index extends Component
|
|||||||
{
|
{
|
||||||
use WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdateStock;
|
use WithCloseModal, WithConfirmation, WithSubscribeNotification, WithToast, WithUpdateStock;
|
||||||
|
|
||||||
|
public function mount()
|
||||||
|
{
|
||||||
|
if (request()->has('order')) {
|
||||||
|
$order = Order::find(request('order'));
|
||||||
|
|
||||||
|
if (! $order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dispatch('fn:print', order: $order->hash);
|
||||||
|
|
||||||
|
$this->js("
|
||||||
|
const url = new URL(window.location);
|
||||||
|
url.search = '';
|
||||||
|
window.history.replaceState({}, '', url);
|
||||||
|
");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function delete(Order $order)
|
public function delete(Order $order)
|
||||||
{
|
{
|
||||||
foreach ($order->items as $item) {
|
foreach ($order->items as $item) {
|
||||||
@ -32,6 +52,27 @@ public function delete(Order $order)
|
|||||||
Flux::modals()->close();
|
Flux::modals()->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[On('fn:print')]
|
||||||
|
public function print(Order $order)
|
||||||
|
{
|
||||||
|
$order->load(['items', 'user', 'user.employee']);
|
||||||
|
|
||||||
|
$this->dispatch(
|
||||||
|
'printReceipt',
|
||||||
|
cashier: $order->user?->employee?->full_name ?? '',
|
||||||
|
customer: $order->customer?->name ?? '',
|
||||||
|
subtotal: $order->subtotal,
|
||||||
|
discount: $order->discount,
|
||||||
|
total: $order->total,
|
||||||
|
items: $order->items->map(function ($item) {
|
||||||
|
$item->name = $item->orderable?->name;
|
||||||
|
$item->total = $item->unit_price * $item->quantity;
|
||||||
|
|
||||||
|
return $item;
|
||||||
|
})->toArray()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.studio.manage.order.index', [
|
return view('livewire.studio.manage.order.index', [
|
||||||
|
|||||||
5
resources/views/components/datatables/print.blade.php
Normal file
5
resources/views/components/datatables/print.blade.php
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<flux:tooltip content="Print">
|
||||||
|
<flux:button variant="primary" color="teal" icon="printer" size="sm"
|
||||||
|
wire:click="$dispatch('fn:print', {order: '{{ $id }}'})">
|
||||||
|
</flux:button>
|
||||||
|
</flux:tooltip>
|
||||||
@ -4,11 +4,14 @@
|
|||||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||||
</div>
|
</div>
|
||||||
@can('create order')
|
@can('create order')
|
||||||
<div>
|
<div class="flex">
|
||||||
<flux:button href="{{ route('studio.manage.order.create') }}" variant="primary" wire:navigate.hover
|
<flux:button href="{{ route('studio.manage.order.create') }}" variant="primary" wire:navigate.hover
|
||||||
class="text-sm">
|
class="text-sm">
|
||||||
Tambah
|
Tambah
|
||||||
</flux:button>
|
</flux:button>
|
||||||
|
<flux:button variant="primary" color="cyan" class="ms-2" wire:click="$dispatch('connectUSB')">
|
||||||
|
Connect USB
|
||||||
|
</flux:button>
|
||||||
</div>
|
</div>
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
@ -19,3 +22,215 @@ class="text-sm">
|
|||||||
|
|
||||||
@include('components.confirmation.delete')
|
@include('components.confirmation.delete')
|
||||||
</flux:main>
|
</flux:main>
|
||||||
|
|
||||||
|
@script
|
||||||
|
<script>
|
||||||
|
document.addEventListener('livewire:navigated', () => {
|
||||||
|
window.printerDevice = null;
|
||||||
|
|
||||||
|
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);
|
||||||
|
console.log('Saved to localStorage:', deviceInfo);
|
||||||
|
|
||||||
|
} 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);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Gagal autoreconnect:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getMaxChar(widthMM) {
|
||||||
|
if (widthMM <= 48) return 32;
|
||||||
|
if (widthMM <= 58) return 42;
|
||||||
|
return 48;
|
||||||
|
}
|
||||||
|
|
||||||
|
Livewire.on('printReceipt', async (event) => {
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
cashier: event.cashier,
|
||||||
|
customer: event.customer,
|
||||||
|
items: event.items,
|
||||||
|
subtotal: event.subtotal,
|
||||||
|
discount: event.discount,
|
||||||
|
total: event.total,
|
||||||
|
};
|
||||||
|
await printReceipt(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Print error:", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function printReceipt(order) {
|
||||||
|
if (!window.printerDevice) {
|
||||||
|
console.error("Printer belum terkoneksi.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const device = window.printerDevice;
|
||||||
|
|
||||||
|
// ESC/POS Commands
|
||||||
|
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(48);
|
||||||
|
|
||||||
|
const line = "-".repeat(MAX) + "\n";
|
||||||
|
|
||||||
|
const formatItem = (name, qty, price, total, isLast) => {
|
||||||
|
let out = name.substring(0, 32) + "\n";
|
||||||
|
|
||||||
|
const left = `${qty} x ${price.toLocaleString()}`;
|
||||||
|
const right = total.toLocaleString();
|
||||||
|
|
||||||
|
const space = 32 - left.length - right.length;
|
||||||
|
out += left + " ".repeat(space > 0 ? space : 1) + right;
|
||||||
|
|
||||||
|
// Kalau terakhir, pakai satu newline.
|
||||||
|
// Kalau bukan, dua newline seperti biasa.
|
||||||
|
out += isLast ? "\n" : "\n\n";
|
||||||
|
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
// =========================================
|
||||||
|
// BUILD TEXT
|
||||||
|
// =========================================
|
||||||
|
|
||||||
|
let text = "";
|
||||||
|
|
||||||
|
// HEADER
|
||||||
|
|
||||||
|
// center and increase font size
|
||||||
|
text += "\x1B\x61\x01";
|
||||||
|
text += "\x1B\x21\x30";
|
||||||
|
|
||||||
|
text += "Yadi Parfum\n\n";
|
||||||
|
|
||||||
|
// reset to normal and left align
|
||||||
|
text += "\x1B\x21\x00";
|
||||||
|
text += "\x1B\x61\x00";
|
||||||
|
|
||||||
|
text += "Kasir : " + order.cashier + "\n";
|
||||||
|
text += "Customer : " + order.customer + "\n";
|
||||||
|
text += line;
|
||||||
|
|
||||||
|
// CONTENT
|
||||||
|
order.items.forEach((i, idx) => {
|
||||||
|
const isLast = idx === order.items.length - 1;
|
||||||
|
text += formatItem(i.name, i.quantity, i.unit_price, i.total, isLast);
|
||||||
|
});
|
||||||
|
|
||||||
|
text += line;
|
||||||
|
|
||||||
|
// FOOTER
|
||||||
|
text += "\x1B\x61\x02";
|
||||||
|
text += `${order.subtotal.toLocaleString()}\n`;
|
||||||
|
text += `-${order.discount.toLocaleString()}\n`;
|
||||||
|
|
||||||
|
// bold teks
|
||||||
|
text += "\x1B\x45\x01";
|
||||||
|
|
||||||
|
text += `${order.total.toLocaleString()}\n`;
|
||||||
|
|
||||||
|
// reset bold
|
||||||
|
text += "\x1B\x45\x00";
|
||||||
|
|
||||||
|
text += line;
|
||||||
|
|
||||||
|
// THANKS
|
||||||
|
text += "\x1B\x61\x01";
|
||||||
|
text += "Terima kasih telah berbelanja dan memilih Yadi Parfum :)\n";
|
||||||
|
|
||||||
|
const textBytes = encoder.encode(text);
|
||||||
|
|
||||||
|
// SEND
|
||||||
|
await device.transferOut(1, reset);
|
||||||
|
await device.transferOut(1, alignLeft);
|
||||||
|
await device.transferOut(1, textBytes);
|
||||||
|
await device.transferOut(1, cut);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endscript
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user