diff --git a/resources/js/app.tsx b/resources/js/app.tsx
index 22c62b6..8bb1ba8 100644
--- a/resources/js/app.tsx
+++ b/resources/js/app.tsx
@@ -8,6 +8,8 @@ import SettingsLayout from '@/layouts/settings/layout';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
+import { PrinterProvider } from '@/contexts/printer-context';
+
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
layout: (name) => {
@@ -26,7 +28,9 @@ createInertiaApp({
withApp(app) {
return (
- {app}
+
+ {app}
+
);
diff --git a/resources/js/contexts/printer-context.tsx b/resources/js/contexts/printer-context.tsx
new file mode 100644
index 0000000..cb3de39
--- /dev/null
+++ b/resources/js/contexts/printer-context.tsx
@@ -0,0 +1,371 @@
+import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react';
+import { toast } from 'sonner';
+
+export type PrinterType = 'bluetooth' | 'usb' | null;
+export type PaperSize = '58' | '80';
+
+interface PrinterState {
+ device: any;
+ type: PrinterType;
+ isConnected: boolean;
+ name: string | null;
+ paperSize: PaperSize;
+}
+
+interface PrinterContextType extends PrinterState {
+ connectBluetooth: () => Promise;
+ connectUsb: () => Promise;
+ disconnect: () => Promise;
+ sendData: (data: Uint8Array) => Promise;
+ setPaperSize: (size: PaperSize) => void;
+}
+
+const PrinterContext = createContext(undefined);
+
+async function findWriteCharacteristic(services: any[]) {
+ for (const service of services) {
+ const characteristics = await service.getCharacteristics();
+
+ for (const char of characteristics) {
+ if (char.properties.write || char.properties.writeWithoutResponse) {
+ return char;
+ }
+ }
+ }
+
+ return null;
+}
+
+export function PrinterProvider({ children }: { children: ReactNode }) {
+ const [state, setState] = useState({
+ device: null,
+ type: null,
+ isConnected: false,
+ name: null,
+ paperSize: (localStorage.getItem('printer_paper_size') as PaperSize) || '58',
+ });
+
+ const [characteristic, setCharacteristic] = useState(null);
+
+ // Save paper size to local storage
+ useEffect(() => {
+ localStorage.setItem('printer_paper_size', state.paperSize);
+ }, [state.paperSize]);
+
+ // USB disconnect listener
+ useEffect(() => {
+ const nav = navigator as any;
+ if (!nav.usb) return;
+
+ const handleUsbDisconnect = (event: any) => {
+ // Check if the disconnected device matches our current device
+ // We use vendorId and productId as a fallback if reference check fails
+ const currentDevice = state.device;
+ if (state.type === 'usb' && currentDevice &&
+ (event.device === currentDevice ||
+ (event.device.vendorId === currentDevice.vendorId &&
+ event.device.productId === currentDevice.productId))) {
+
+ setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null }));
+ toast.error('Printer USB terputus');
+ }
+ };
+
+ nav.usb.addEventListener('disconnect', handleUsbDisconnect);
+ return () => nav.usb.removeEventListener('disconnect', handleUsbDisconnect);
+ }, [state.type, state.device]);
+
+ // Bluetooth disconnect listener
+ useEffect(() => {
+ if (state.type === 'bluetooth' && state.device) {
+ const device = state.device;
+ const handleDisconnect = () => {
+ setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null }));
+ setCharacteristic(null);
+ toast.error('Printer Bluetooth terputus');
+ };
+
+ device.addEventListener('gattserverdisconnected', handleDisconnect);
+ return () => device.removeEventListener('gattserverdisconnected', handleDisconnect);
+ }
+ }, [state.type, state.device]);
+
+ // Auto-reconnect on mount
+ useEffect(() => {
+ const autoReconnect = async () => {
+ const nav = navigator as any;
+ const lastType = localStorage.getItem('last_printer_type');
+
+ if (lastType === 'bluetooth' && nav.bluetooth?.getDevices) {
+ try {
+ const devices = await nav.bluetooth.getDevices();
+ if (devices.length > 0) {
+ const device = devices[0];
+ const server = await device.gatt?.connect();
+ const services = await server.getPrimaryServices();
+ const writeChar = await findWriteCharacteristic(services);
+ if (writeChar) {
+ setCharacteristic(writeChar);
+ setState(prev => ({
+ ...prev,
+ device,
+ type: 'bluetooth',
+ isConnected: true,
+ name: device.name || 'Bluetooth Printer',
+ }));
+ }
+ }
+ } catch (e) {
+ console.error('Auto-reconnect bluetooth failed:', e);
+ }
+ } else if (lastType === 'usb' && nav.usb?.getDevices) {
+ try {
+ const devices = await nav.usb.getDevices();
+ if (devices.length > 0) {
+ const device = devices[0];
+ await device.open();
+ if (device.configuration === null) await device.selectConfiguration(1);
+ const iface = device.configuration.interfaces[0];
+ await device.claimInterface(iface.interfaceNumber);
+
+ setState(prev => ({
+ ...prev,
+ device,
+ type: 'usb',
+ isConnected: true,
+ name: device.productName || 'USB Printer',
+ }));
+ }
+ } catch (e) {
+ console.error('Auto-reconnect usb failed:', e);
+ }
+ }
+ };
+
+ autoReconnect();
+ }, []);
+
+ const connectBluetooth = useCallback(async () => {
+ try {
+ const nav = navigator as any;
+
+ if (!nav.bluetooth) {
+ toast.error('Bluetooth tidak didukung di browser ini');
+
+ return;
+ }
+
+ const device = await nav.bluetooth.requestDevice({
+ acceptAllDevices: true,
+ optionalServices: [
+ '000018f0-0000-1000-8000-00805f9b34fb',
+ '49535343-fe7d-41aa-83b1-d10935904914',
+ '0000ff00-0000-1000-8000-00805f9b34fb',
+ '00001101-0000-1000-8000-00805f9b34fb'
+ ]
+ });
+
+ const server = await device.gatt?.connect();
+ const services = await server.getPrimaryServices();
+
+ const writeChar = await findWriteCharacteristic(services);
+
+ if (!writeChar) {
+ throw new Error('Tidak dapat menemukan characteristic untuk menulis data');
+ }
+
+ setCharacteristic(writeChar);
+ setState(prev => ({
+ ...prev,
+ device,
+ type: 'bluetooth',
+ isConnected: true,
+ name: device.name || 'Bluetooth Printer',
+ }));
+
+ localStorage.setItem('last_printer_type', 'bluetooth');
+ toast.success(`Terhubung ke ${device.name || 'Printer Bluetooth'}`);
+
+ } catch (error: any) {
+ console.error('Bluetooth connection error:', error);
+
+ if (error.name !== 'NotFoundError') {
+ toast.error(error.message || 'Gagal menghubungkan printer bluetooth');
+ }
+ }
+ }, []);
+
+ const connectUsb = useCallback(async () => {
+ try {
+ const nav = navigator as any;
+
+ if (!nav.usb) {
+ toast.error('USB tidak didukung di browser ini');
+
+ return;
+ }
+
+ const device = await nav.usb.requestDevice({
+ filters: []
+ });
+
+ await device.open();
+
+ if (device.configuration === null) {
+ await device.selectConfiguration(1);
+ }
+
+ const iface = device.configuration.interfaces[0];
+ await device.claimInterface(iface.interfaceNumber);
+
+ setState(prev => ({
+ ...prev,
+ device,
+ type: 'usb',
+ isConnected: true,
+ name: device.productName || 'USB Printer',
+ }));
+
+ localStorage.setItem('last_printer_type', 'usb');
+ toast.success(`Terhubung ke ${device.productName || 'Printer USB'}`);
+
+ } catch (error: any) {
+ console.error('USB connection error:', error);
+
+ if (error.name !== 'NotFoundError') {
+ toast.error(error.message || 'Gagal menghubungkan printer USB');
+ }
+ }
+ }, []);
+
+ const setPaperSize = useCallback((size: PaperSize) => {
+ setState(prev => ({ ...prev, paperSize: size }));
+ }, []);
+
+ const sendData = useCallback(async (data: Uint8Array) => {
+ if (!state.isConnected || !state.device) {
+ toast.error('Printer belum terhubung');
+
+ return;
+ }
+
+ try {
+ if (state.type === 'bluetooth') {
+ const device = state.device;
+ let activeChar = characteristic;
+
+ // Try to reconnect if GATT is disconnected
+ if (!device.gatt?.connected) {
+ toast.loading('Menghubungkan kembali...', { id: 'printer-reconnect' });
+ try {
+ const server = await device.gatt?.connect();
+ const services = await server.getPrimaryServices();
+ const writeChar = await findWriteCharacteristic(services);
+
+ if (writeChar) {
+ setCharacteristic(writeChar);
+ activeChar = writeChar;
+ toast.success('Printer terhubung kembali', { id: 'printer-reconnect' });
+ } else {
+ throw new Error('Gagal menemukan characteristic');
+ }
+ } catch (e) {
+ toast.error('Printer terputus', { id: 'printer-reconnect' });
+ setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null }));
+ setCharacteristic(null);
+
+ return;
+ }
+ }
+
+ if (!activeChar) {
+ throw new Error('Characteristic tidak ditemukan');
+ }
+
+ // Bluetooth printers often have small buffers and limited MTU.
+ // 20 bytes is the standard BLE MTU.
+ const chunkSize = 20;
+
+ for (let i = 0; i < data.length; i += chunkSize) {
+ const chunk = data.slice(i, i + chunkSize);
+
+ // Prefer writeValue (with response) for better reliability
+ // it acts as a primitive flow control
+ await activeChar.writeValue(chunk);
+
+ // Add a tiny delay between chunks if the data is large to avoid buffer overflow
+ // especially for cheaper printers
+ if (i % 100 === 0 && i > 0) {
+ await new Promise(resolve => setTimeout(resolve, 50));
+ }
+ }
+ } else if (state.type === 'usb') {
+ const device = state.device as any;
+
+ try {
+ // Check if device is still open
+ if (!device.opened) {
+ await device.open();
+ }
+
+ const endpoint = device.configuration.interfaces[0].alternates[0].endpoints.find((e: any) => e.direction === 'out' && e.type === 'bulk');
+
+ if (!endpoint) {
+ throw new Error('Tidak dapat menemukan USB endpoint');
+ }
+
+ await device.transferOut(endpoint.endpointNumber, data);
+ } catch (e) {
+ console.error('USB print error:', e);
+ setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null }));
+ toast.error('Printer USB terputus');
+
+ return;
+ }
+ }
+ } catch (error: any) {
+ console.error('Print error:', error);
+ toast.error('Gagal mengirim data ke printer: ' + (error.message || 'Error tidak diketahui'));
+ }
+ }, [state.isConnected, state.device, state.type, characteristic]);
+
+ const disconnect = useCallback(async () => {
+ if (state.type === 'bluetooth' && state.device) {
+ if (state.device.gatt?.connected) {
+ state.device.gatt.disconnect();
+ }
+ } else if (state.type === 'usb' && state.device) {
+ try {
+ await state.device.close();
+ } catch (e) {
+ console.error('Error closing USB device:', e);
+ }
+ }
+
+ localStorage.removeItem('last_printer_type');
+ setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null }));
+ setCharacteristic(null);
+ toast.info('Printer terputus');
+ }, [state.device, state.type]);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePrinterContext() {
+ const context = useContext(PrinterContext);
+ if (context === undefined) {
+ throw new Error('usePrinterContext must be used within a PrinterProvider');
+ }
+ return context;
+}
diff --git a/resources/js/hooks/use-printer.ts b/resources/js/hooks/use-printer.ts
index a6444cd..2bac833 100644
--- a/resources/js/hooks/use-printer.ts
+++ b/resources/js/hooks/use-printer.ts
@@ -1,224 +1,7 @@
-import { useState, useCallback } from 'react';
-import { toast } from 'sonner';
+import { usePrinterContext } from '@/contexts/printer-context';
-// Type definitions for Web Bluetooth and Web USB
-// This is to satisfy TypeScript if the global types are missing
-
-
-export type PrinterType = 'bluetooth' | 'usb' | null;
-export type PaperSize = '58' | '80';
-
-interface PrinterState {
- device: any; // Using any to avoid complex type issues with experimental APIs
- type: PrinterType;
- isConnected: boolean;
- name: string | null;
- paperSize: PaperSize;
-}
+export type { PrinterType, PaperSize } from '@/contexts/printer-context';
export function usePrinter() {
- const [state, setState] = useState(() => {
- return {
- device: null,
- type: null,
- isConnected: false,
- name: null,
- paperSize: '58',
- };
- });
-
- const [characteristic, setCharacteristic] = useState(null);
-
- const connectBluetooth = useCallback(async () => {
- try {
- const nav = navigator as any;
-
- if (!nav.bluetooth) {
- toast.error('Bluetooth tidak didukung di browser ini');
-
- return;
- }
-
- const device = await nav.bluetooth.requestDevice({
- acceptAllDevices: true,
- optionalServices: ['000018f0-0000-1000-8000-00805f9b34fb', '49535343-fe7d-41aa-83b1-d10935904914', '0000ff00-0000-1000-8000-00805f9b34fb']
- });
-
- const server = await device.gatt?.connect();
- const services = await server.getPrimaryServices();
-
- const writeChar = await findWriteCharacteristic(services);
-
- if (!writeChar) {
- throw new Error('Tidak dapat menemukan characteristic untuk menulis data');
- }
-
- setCharacteristic(writeChar);
- setState(prev => ({
- ...prev,
- device,
- type: 'bluetooth',
- isConnected: true,
- name: device.name || 'Bluetooth Printer',
- }));
-
- toast.success(`Terhubung ke ${device.name || 'Printer Bluetooth'}`);
-
- device.addEventListener('gattserverdisconnected', () => {
- setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null }));
- setCharacteristic(null);
- toast.error('Printer Bluetooth terputus');
- });
-
- } catch (error: any) {
- console.error('Bluetooth connection error:', error);
-
- if (error.name !== 'NotFoundError') {
- toast.error(error.message || 'Gagal menghubungkan printer bluetooth');
- }
- }
- }, []);
-
- const connectUsb = useCallback(async () => {
- try {
- const nav = navigator as any;
-
- if (!nav.usb) {
- toast.error('USB tidak didukung di browser ini');
-
- return;
- }
-
- const device = await nav.usb.requestDevice({
- filters: []
- });
-
- await device.open();
-
- if (device.configuration === null) {
- await device.selectConfiguration(1);
- }
-
- const iface = device.configuration.interfaces[0];
- await device.claimInterface(iface.interfaceNumber);
-
- setState(prev => ({
- ...prev,
- device,
- type: 'usb',
- isConnected: true,
- name: device.productName || 'USB Printer',
- }));
-
- toast.success(`Terhubung ke ${device.productName || 'Printer USB'}`);
-
- } catch (error: any) {
- console.error('USB connection error:', error);
-
- if (error.name !== 'NotFoundError') {
- toast.error(error.message || 'Gagal menghubungkan printer USB');
- }
- }
- }, []);
-
- const setPaperSize = useCallback((size: PaperSize) => {
- setState(prev => ({ ...prev, paperSize: size }));
- }, []);
-
- const sendData = useCallback(async (data: Uint8Array) => {
- if (!state.isConnected || !state.device) {
- toast.error('Printer belum terhubung');
-
- return;
- }
-
- try {
- if (state.type === 'bluetooth') {
- const device = state.device;
-
- // Try to reconnect if GATT is disconnected
- if (!device.gatt?.connected) {
- toast.loading('Menghubungkan kembali...', { id: 'printer-reconnect' });
- const server = await device.gatt?.connect();
- const services = await server.getPrimaryServices();
- const writeChar = await findWriteCharacteristic(services);
-
- if (writeChar) {
- setCharacteristic(writeChar);
- toast.success('Printer terhubung kembali', { id: 'printer-reconnect' });
- } else {
- toast.error('Gagal menghubungkan kembali printer', { id: 'printer-reconnect' });
-
- return;
- }
- }
-
- if (!characteristic) {
-throw new Error('Characteristic tidak ditemukan');
-}
-
- const chunkSize = 20;
-
- for (let i = 0; i < data.length; i += chunkSize) {
- const chunk = data.slice(i, i + chunkSize);
-
- // Use writeValueWithoutResponse for better stability if supported
- if (characteristic.writeValueWithoutResponse) {
- await characteristic.writeValueWithoutResponse(chunk);
- } else {
- await characteristic.writeValue(chunk);
- }
- }
- } else if (state.type === 'usb') {
- const device = state.device as any;
- const endpoint = device.configuration.interfaces[0].alternates[0].endpoints.find((e: any) => e.direction === 'out' && e.type === 'bulk');
-
- if (!endpoint) {
-throw new Error('Tidak dapat menemukan USB endpoint');
-}
-
- await device.transferOut(endpoint.endpointNumber, data);
- }
- } catch (error: any) {
- console.error('Print error:', error);
- toast.error('Gagal mengirim data ke printer: ' + (error.message || 'Error tidak diketahui'));
- }
- }, [state.isConnected, state.device, state.type, characteristic]);
-
- const disconnect = useCallback(async () => {
- if (state.type === 'bluetooth' && state.device) {
- if (state.device.gatt?.connected) {
- state.device.gatt.disconnect();
- }
- } else if (state.type === 'usb' && state.device) {
- await state.device.close();
- }
-
- setState(prev => ({ ...prev, device: null, type: null, isConnected: false, name: null }));
- setCharacteristic(null);
- toast.info('Printer terputus');
- }, [state.device, state.type]);
-
- return {
- ...state,
- connectBluetooth,
- connectUsb,
- disconnect,
- sendData,
- setPaperSize,
- };
-}
-
-async function findWriteCharacteristic(services: any[]) {
- for (const service of services) {
- const characteristics = await service.getCharacteristics();
-
- for (const char of characteristics) {
- if (char.properties.write || char.properties.writeWithoutResponse) {
- return char;
- }
- }
- }
-
- return null;
+ return usePrinterContext();
}
diff --git a/resources/js/pages/admin/manage/order/hooks/use-order-print.ts b/resources/js/pages/admin/manage/order/hooks/use-order-print.ts
index 963c7a4..595ecdf 100644
--- a/resources/js/pages/admin/manage/order/hooks/use-order-print.ts
+++ b/resources/js/pages/admin/manage/order/hooks/use-order-print.ts
@@ -84,7 +84,7 @@ export function useOrderPrint() {
.feed(1)
.line('Terima Kasih')
.line('Selamat Belanja Kembali')
- .feed(1)
+ .feed(3)
.cut();
await sendData(result.encode());