dstpabuaran.com/resources/js/hooks/use-thermal-printer.ts
Yoga Pangestu 85556f8779 feat: enhance product management with approval and rejection workflows
- Added approval and rejection functionality for products, including new routes and methods in the ProductController.
- Implemented UI changes to display product status (pending, rejected) with appropriate badges and actions.
- Introduced a RejectDialog component for providing rejection reasons.
- Updated product columns to handle new actions for approving and rejecting products.
- Enhanced variant management to restrict actions based on product status.
- Refactored various components to improve code organization and readability.
2026-08-07 11:23:41 +07:00

378 lines
10 KiB
TypeScript

import ReceiptPrinterEncoder from '@point-of-sale/receipt-printer-encoder';
import { useCallback, useRef, useState } from 'react';
export type PrinterConnectionType = 'bluetooth' | 'usb';
export type PaperWidth = 58 | 80;
interface PrinterState {
connected: boolean;
connectionType: PrinterConnectionType | null;
printerName: string | null;
}
/* ── Receipt Builder (persis seperti contoh Vue) ── */
export function getPaperColumns(paperWidth: PaperWidth): number {
return paperWidth === 58 ? 32 : 48;
}
function dashedLine(columns: number): string {
return '-'.repeat(columns);
}
export type OrderReceiptItem = {
product_name: string;
variant_name: string;
quantity: string;
unit_price: string;
subtotal: string;
};
export type OrderReceiptData = {
order_number: string;
created_at: string;
customer_name: string | null;
cashier_name: string;
items: OrderReceiptItem[];
subtotal: string;
discount: string;
nego_price: string | null;
total_amount: string;
notes: string | null;
};
export type EncodeOrderReceiptOptions = {
storeName: string;
storeAddress: string;
paperWidth: PaperWidth;
};
export function encodeOrderReceipt(
order: OrderReceiptData,
options: EncodeOrderReceiptOptions,
): Uint8Array {
const columns = getPaperColumns(options.paperWidth);
const amountColumnWidth = 14;
const labelColumnWidth = columns - amountColumnWidth;
const encoder = new ReceiptPrinterEncoder({ columns });
encoder
.initialize()
.align('center')
.bold(true)
.text('')
.newline()
.text(options.storeName)
.newline()
.bold(false)
.newline();
for (const addressLine of options.storeAddress.split('\n')) {
const trimmed = addressLine.trim();
if (trimmed) {
encoder.align('center').text(trimmed).newline();
}
}
encoder.align('center').line(dashedLine(columns));
encoder
.align('left')
.line(`No : ${order.order_number}`)
.line(`Tgl : ${order.created_at}`);
if (order.customer_name) {
encoder.line(`Cust : ${order.customer_name}`);
}
encoder
.line(`Kasir: ${order.cashier_name}`)
.line(dashedLine(columns));
for (const item of order.items) {
encoder
.line(item.product_name)
.line(` ${item.variant_name}`)
.table(
[
{ width: labelColumnWidth, align: 'left' },
{ width: amountColumnWidth, align: 'right' },
],
[
[
`${item.quantity} x ${item.unit_price}`,
(rowEncoder: any) =>
rowEncoder
.bold()
.text(item.subtotal)
.bold(false),
],
],
)
.newline();
}
encoder.line(dashedLine(columns));
const summaryRows = [
['Subtotal', order.subtotal],
['Diskon', order.discount],
];
if (order.nego_price) {
summaryRows.push(['Harga Nego', order.nego_price]);
}
summaryRows.push(['Total', order.total_amount]);
encoder.table(
[
{ width: labelColumnWidth, align: 'left' },
{ width: amountColumnWidth, align: 'right' },
],
summaryRows,
);
encoder
.newline()
.line(dashedLine(columns))
.align('center')
.line('Terima kasih atas kunjungannya')
.line('Silakan berbelanja kembali')
.newline(3)
.cut();
return encoder.encode();
}
/* ── Printer Connection Hook ── */
export function useThermalPrinter() {
const [state, setState] = useState<PrinterState>({
connected: false,
connectionType: null,
printerName: null,
});
const bluetoothDeviceRef = useRef<BluetoothDevice | null>(null);
const bluetoothCharacteristicRef =
useRef<BluetoothCharacteristic | null>(null);
const usbDeviceRef = useRef<USBDevice | null>(null);
const disconnect = useCallback(async () => {
try {
if (
state.connectionType === 'bluetooth' &&
bluetoothDeviceRef.current
) {
bluetoothDeviceRef.current.gatt?.disconnect();
bluetoothDeviceRef.current = null;
bluetoothCharacteristicRef.current = null;
}
if (state.connectionType === 'usb' && usbDeviceRef.current) {
usbDeviceRef.current.close();
usbDeviceRef.current = null;
}
} catch {
// silent
}
setState({
connected: false,
connectionType: null,
printerName: null,
});
}, [state.connectionType]);
const connectBluetooth = useCallback(async () => {
if (!navigator.bluetooth) {
throw new Error(
'Web Bluetooth tidak didukung di browser ini. Gunakan Chrome atau Edge.',
);
}
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true,
optionalServices: [
'000018f0-0000-1000-8000-00805f9b34fb',
'00001800-0000-1000-8000-00805f9b34fb',
'00001801-0000-1000-8000-00805f9b34fb',
'00001101-0000-1000-8000-00805f9b34fb',
'0000ffe0-0000-1000-8000-00805f9b34fb',
'0000fee7-0000-1000-8000-00805f9b34fb',
],
});
const server = await device.gatt?.connect();
if (!server) {
throw new Error('Gagal terhubung ke perangkat Bluetooth.');
}
let characteristic: BluetoothCharacteristic | null = null;
const services = await server.getPrimaryServices();
for (const svc of services) {
try {
const chars = await svc.getCharacteristics();
const writable = chars.find(
(c) =>
c.properties.writeWithoutResponse ||
c.properties.write,
);
if (writable) {
characteristic = writable;
break;
}
} catch {
// skip
}
}
if (!characteristic) {
throw new Error(
'Tidak ditemukan characteristic yang bisa ditulis di printer ini.',
);
}
bluetoothDeviceRef.current = device;
bluetoothCharacteristicRef.current = characteristic;
setState({
connected: true,
connectionType: 'bluetooth',
printerName: device.name ?? 'Printer Bluetooth',
});
return device.name ?? 'Printer Bluetooth';
}, []);
const connectUSB = useCallback(async () => {
if (!navigator.usb) {
throw new Error(
'Web USB tidak didukung di browser ini. Gunakan Chrome atau Edge.',
);
}
const device = await navigator.usb.requestDevice({ filters: [] });
await device.open();
if (device.configuration === null) {
await device.selectConfiguration(1);
}
const iface =
device.configuration?.interfaces.find((i) =>
i.alternate.endpoints.some((e) => e.direction === 'out'),
) ?? device.configuration?.interfaces[0];
if (iface) {
await device.claimInterface(iface.interfaceNumber);
}
usbDeviceRef.current = device;
setState({
connected: true,
connectionType: 'usb',
printerName: device.productName ?? 'Printer USB',
});
return device.productName ?? 'Printer USB';
}, []);
const sendToBluetooth = useCallback(async (data: Uint8Array) => {
const characteristic = bluetoothCharacteristicRef.current;
if (!characteristic) {
throw new Error(
'Printer Bluetooth tidak terhubung.',
);
}
const useWriteWithoutResponse =
characteristic.properties.writeWithoutResponse;
const chunkSize = 128;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
if (useWriteWithoutResponse) {
await characteristic.writeValueWithoutResponse(chunk);
} else {
await characteristic.writeValueWithResponse(chunk);
}
await new Promise((r) => setTimeout(r, 10));
}
}, []);
const sendToUSB = useCallback(async (data: Uint8Array) => {
const device = usbDeviceRef.current;
if (!device) {
throw new Error('Printer USB tidak terhubung.');
}
const iface = device.configuration?.interfaces.find((i) =>
i.alternate.endpoints.some((e) => e.direction === 'out'),
);
const endpoint =
iface?.alternate.endpoints.find(
(e) => e.direction === 'out',
)?.endpointNumber ?? 1;
const chunkSize = 512;
for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.slice(i, i + chunkSize);
await device.transferOut(endpoint, chunk);
}
}, []);
const print = useCallback(
async (data: Uint8Array) => {
if (!state.connected) {
throw new Error('Tidak ada printer yang terhubung.');
}
if (state.connectionType === 'bluetooth') {
await sendToBluetooth(data);
} else if (state.connectionType === 'usb') {
await sendToUSB(data);
}
},
[state.connected, state.connectionType, sendToBluetooth, sendToUSB],
);
const connect = useCallback(
async (type: PrinterConnectionType) => {
if (state.connected) {
await disconnect();
}
if (type === 'bluetooth') {
return await connectBluetooth();
}
return await connectUSB();
},
[state.connected, disconnect, connectBluetooth, connectUSB],
);
return {
connected: state.connected,
connectionType: state.connectionType,
printerName: state.printerName,
connect,
disconnect,
print,
};
}