feat: implement printer settings component and refactor order printing logic for improved functionality

This commit is contained in:
Yoga Pangestu 2026-04-23 15:11:05 +07:00
parent 221f796696
commit 93828b6da9
4 changed files with 203 additions and 142 deletions

View File

@ -0,0 +1,97 @@
import { usePrinter } from '@/hooks/use-printer';
import { EscPosEncoder } from '@/lib/esc-pos-encoder';
import type { Order } from '@/types/order';
import { format } from 'date-fns';
import { id } from 'date-fns/locale';
import { useCallback } from 'react';
import { toast } from 'sonner';
export function useOrderPrint() {
const {
isConnected,
name,
connectBluetooth,
connectUsb,
disconnect,
sendData,
paperSize,
setPaperSize
} = usePrinter();
const handlePrint = useCallback(async (order: Order) => {
if (!isConnected) {
toast.error('Hubungkan printer terlebih dahulu');
return;
}
const width = paperSize === '58' ? 32 : 48;
const line = '-'.repeat(width);
const encoder = new EscPosEncoder();
const result = encoder
.initialize()
.align(1)
.size(1)
.bold(true)
.line('VN Grup')
.size(0)
.bold(false)
.line(line)
.align(0)
.line(`No. Invoice : ${order.invoice_number}`)
.line(`Tanggal : ${format(new Date(order.created_at), 'dd MMMM yyyy HH:mm', { locale: id })}`)
.line(`Pelanggan : ${order.customer_name}`)
.line(line);
order.items?.forEach((item, index) => {
if (index > 0) {
result.line();
}
result.line(item.product?.name || 'Produk');
const qtyPrice = `${item.qty} x ${item.price_formatted}`;
const subtotal = item.total_formatted;
const spaces = width - qtyPrice.length - subtotal.length;
result.line(qtyPrice + ' '.repeat(Math.max(0, spaces)) + subtotal);
});
result.line(line);
const discountAmount = Number(order.discount || 0);
if (discountAmount > 0) {
const discountLabel = 'Diskon:';
const discountVal = `- Rp ${discountAmount.toLocaleString('id-ID')}`;
const dSpaces = width - discountLabel.length - discountVal.length;
result.line(discountLabel + ' '.repeat(Math.max(0, dSpaces)) + discountVal);
}
const totalLabel = 'TOTAL:';
const totalVal = order.total_formatted;
const tSpaces = width - totalLabel.length - totalVal.length;
result.bold(true).line(totalLabel + ' '.repeat(Math.max(0, tSpaces)) + totalVal).bold(false);
result
.line(line)
.align(1)
.feed(1)
.line('Terima Kasih')
.line('Selamat Belanja Kembali')
.feed(1)
.cut();
await sendData(result.encode());
}, [isConnected, sendData, paperSize]);
return {
isConnected,
name,
paperSize,
connectBluetooth,
connectUsb,
disconnect,
setPaperSize,
handlePrint
};
}

View File

@ -1,33 +1,16 @@
import { Head, Link } from '@inertiajs/react';
import { format } from 'date-fns';
import { id } from 'date-fns/locale';
import { Bluetooth, Plus, Printer, Trash2, Unplug, Usb } from 'lucide-react';
import { useCallback } from 'react';
import { toast } from 'sonner';
import { DataTable } from '@/components/data-table';
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Head, Link } from '@inertiajs/react';
import { Plus, Trash2 } from 'lucide-react';
import { usePrinter } from '@/hooks/use-printer';
import { EscPosEncoder } from '@/lib/esc-pos-encoder';
import * as orderRoutes from '@/routes/order';
import type { Order } from '@/types/order';
import { useOrderIndex } from './hooks/use-order-index';
import { useOrderPrint } from './hooks/use-order-print';
import { getColumns } from './partials/columns';
import { PrinterSettings } from './partials/printer-settings';
export default function OrderIndex({ orders }: { orders: Order[] }) {
const {
@ -48,79 +31,13 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
const {
isConnected,
name,
paperSize,
connectBluetooth,
connectUsb,
disconnect,
sendData,
paperSize,
setPaperSize
} = usePrinter();
const handlePrint = useCallback(async (order: Order) => {
if (!isConnected) {
toast.error('Hubungkan printer terlebih dahulu');
return;
}
const width = paperSize === '58' ? 32 : 48;
const line = '-'.repeat(width);
const encoder = new EscPosEncoder();
const result = encoder
.initialize()
.align(1)
.size(1)
.bold(true)
.line('VN Grup')
.size(0)
.bold(false)
.line(line)
.align(0)
.line(`No. Invoice : ${order.invoice_number}`)
.line(`Tanggal : ${format(new Date(order.created_at), 'dd MMMM yyyy HH:mm', { locale: id })}`)
.line(`Pelanggan : ${order.customer_name}`)
.line(line);
order.items?.forEach((item, index) => {
if (index > 0) {
result.line();
}
result.line(item.product?.name || 'Produk');
const qtyPrice = `${item.qty} x ${item.price_formatted}`;
const subtotal = item.total_formatted;
const spaces = width - qtyPrice.length - subtotal.length;
result.line(qtyPrice + ' '.repeat(Math.max(0, spaces)) + subtotal);
});
result.line(line);
const discountAmount = Number(order.discount || 0);
if (discountAmount > 0) {
const discountLabel = 'Diskon:';
const discountVal = `- Rp ${discountAmount.toLocaleString('id-ID')}`;
const dSpaces = width - discountLabel.length - discountVal.length;
result.line(discountLabel + ' '.repeat(Math.max(0, dSpaces)) + discountVal);
}
const totalLabel = 'TOTAL:';
const totalVal = order.total_formatted;
const tSpaces = width - totalLabel.length - totalVal.length;
result.bold(true).line(totalLabel + ' '.repeat(Math.max(0, tSpaces)) + totalVal).bold(false);
result
.line(line)
.align(1)
.feed(1)
.line('Terima Kasih')
.line('Selamat Belanja Kembali')
.feed(1)
.cut();
await sendData(result.encode());
}, [isConnected, sendData, paperSize]);
setPaperSize,
handlePrint
} = useOrderPrint();
const columns = getColumns({ onDelete, onPrint: handlePrint });
@ -133,50 +50,15 @@ result.line();
<h1 className="text-3xl font-bold tracking-tight text-foreground">Pesanan</h1>
</div>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={isConnected ? "outline" : "secondary"} className="gap-2">
<Printer className="size-4" />
{isConnected ? name : 'Hubungkan Printer'}
{isConnected && <div className="size-2 rounded-full bg-green-500 animate-pulse" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Koneksi Printer</DropdownMenuLabel>
<DropdownMenuSeparator />
{isConnected ? (
<>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
Ukuran Kertas: {paperSize}mm
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={paperSize} onValueChange={(v) => setPaperSize(v as any)}>
<DropdownMenuRadioItem value="58">58mm</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="80">80mm</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={disconnect} variant="destructive">
<Unplug className="mr-2 size-4" />
Putus Koneksi
</DropdownMenuItem>
</>
) : (
<>
<DropdownMenuItem onClick={connectBluetooth}>
<Bluetooth className="mr-2 size-4" />
Bluetooth
</DropdownMenuItem>
<DropdownMenuItem onClick={connectUsb}>
<Usb className="mr-2 size-4" />
USB
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<PrinterSettings
isConnected={isConnected}
name={name}
paperSize={paperSize}
onConnectBluetooth={connectBluetooth}
onConnectUsb={connectUsb}
onDisconnect={disconnect}
onPaperSizeChange={setPaperSize}
/>
<Link href={orderRoutes.create().url}>
<Button className="gap-2">

View File

@ -1,14 +1,14 @@
import { Link } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table';
import { format } from 'date-fns';
import { id } from 'date-fns/locale';
import { Pencil, Trash2, Printer } from 'lucide-react';
import { DataTableColumnHeader } from '@/components/data-table-column-header';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import * as orderRoutes from '@/routes/order';
import type { Order } from '@/types/order';
import { Link } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table';
import { format } from 'date-fns';
import { id } from 'date-fns/locale';
import { Pencil, Printer, Trash2 } from 'lucide-react';
interface ColumnProps {
onDelete: (order: Order) => void;
@ -86,8 +86,8 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
</span>
<Badge className="text-[10px] h-4 py-0" variant={
row.original.order_status === 'delivered' ? 'default' :
row.original.order_status === 'pending' ? 'secondary' :
row.original.order_status === 'cancelled' ? 'destructive' : 'outline'
row.original.order_status === 'pending' ? 'secondary' :
row.original.order_status === 'cancelled' ? 'destructive' : 'outline'
}>
{row.original.order_status}
</Badge>

View File

@ -0,0 +1,82 @@
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Bluetooth, Printer, Unplug, Usb } from 'lucide-react';
interface PrinterSettingsProps {
isConnected: boolean;
name: string | null;
paperSize: string;
onConnectBluetooth: () => void;
onConnectUsb: () => void;
onDisconnect: () => void;
onPaperSizeChange: (size: any) => void;
}
export function PrinterSettings({
isConnected,
name,
paperSize,
onConnectBluetooth,
onConnectUsb,
onDisconnect,
onPaperSizeChange
}: PrinterSettingsProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={isConnected ? "outline" : "secondary"} className="gap-2">
<Printer className="size-4" />
{isConnected ? name : 'Hubungkan Printer'}
{isConnected && <div className="size-2 rounded-full bg-green-500 animate-pulse" />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel>Koneksi Printer</DropdownMenuLabel>
<DropdownMenuSeparator />
{isConnected ? (
<>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
Ukuran Kertas: {paperSize}mm
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup value={paperSize} onValueChange={onPaperSizeChange}>
<DropdownMenuRadioItem value="58">58mm</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="80">80mm</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onDisconnect} variant="destructive">
<Unplug className="mr-2 size-4" />
Putus Koneksi
</DropdownMenuItem>
</>
) : (
<>
<DropdownMenuItem onClick={onConnectBluetooth}>
<Bluetooth className="mr-2 size-4" />
Bluetooth
</DropdownMenuItem>
<DropdownMenuItem onClick={onConnectUsb}>
<Usb className="mr-2 size-4" />
USB
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}