feat: implement printer settings component and refactor order printing logic for improved functionality
This commit is contained in:
parent
221f796696
commit
93828b6da9
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -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 { DataTable } from '@/components/data-table';
|
||||||
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
|
import { DeleteConfirmation } from '@/components/modal/delete-confirmation';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import {
|
import { Head, Link } from '@inertiajs/react';
|
||||||
DropdownMenu,
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuLabel,
|
|
||||||
DropdownMenuRadioGroup,
|
|
||||||
DropdownMenuRadioItem,
|
|
||||||
DropdownMenuSeparator,
|
|
||||||
DropdownMenuSub,
|
|
||||||
DropdownMenuSubContent,
|
|
||||||
DropdownMenuSubTrigger,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
|
|
||||||
import { usePrinter } from '@/hooks/use-printer';
|
|
||||||
import { EscPosEncoder } from '@/lib/esc-pos-encoder';
|
|
||||||
import * as orderRoutes from '@/routes/order';
|
import * as orderRoutes from '@/routes/order';
|
||||||
import type { Order } from '@/types/order';
|
import type { Order } from '@/types/order';
|
||||||
import { useOrderIndex } from './hooks/use-order-index';
|
import { useOrderIndex } from './hooks/use-order-index';
|
||||||
|
import { useOrderPrint } from './hooks/use-order-print';
|
||||||
import { getColumns } from './partials/columns';
|
import { getColumns } from './partials/columns';
|
||||||
|
import { PrinterSettings } from './partials/printer-settings';
|
||||||
|
|
||||||
export default function OrderIndex({ orders }: { orders: Order[] }) {
|
export default function OrderIndex({ orders }: { orders: Order[] }) {
|
||||||
const {
|
const {
|
||||||
@ -48,79 +31,13 @@ export default function OrderIndex({ orders }: { orders: Order[] }) {
|
|||||||
const {
|
const {
|
||||||
isConnected,
|
isConnected,
|
||||||
name,
|
name,
|
||||||
|
paperSize,
|
||||||
connectBluetooth,
|
connectBluetooth,
|
||||||
connectUsb,
|
connectUsb,
|
||||||
disconnect,
|
disconnect,
|
||||||
sendData,
|
setPaperSize,
|
||||||
paperSize,
|
handlePrint
|
||||||
setPaperSize
|
} = useOrderPrint();
|
||||||
} = 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]);
|
|
||||||
|
|
||||||
const columns = getColumns({ onDelete, onPrint: handlePrint });
|
const columns = getColumns({ onDelete, onPrint: handlePrint });
|
||||||
|
|
||||||
@ -133,50 +50,15 @@ result.line();
|
|||||||
<h1 className="text-3xl font-bold tracking-tight text-foreground">Pesanan</h1>
|
<h1 className="text-3xl font-bold tracking-tight text-foreground">Pesanan</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<DropdownMenu>
|
<PrinterSettings
|
||||||
<DropdownMenuTrigger asChild>
|
isConnected={isConnected}
|
||||||
<Button variant={isConnected ? "outline" : "secondary"} className="gap-2">
|
name={name}
|
||||||
<Printer className="size-4" />
|
paperSize={paperSize}
|
||||||
{isConnected ? name : 'Hubungkan Printer'}
|
onConnectBluetooth={connectBluetooth}
|
||||||
{isConnected && <div className="size-2 rounded-full bg-green-500 animate-pulse" />}
|
onConnectUsb={connectUsb}
|
||||||
</Button>
|
onDisconnect={disconnect}
|
||||||
</DropdownMenuTrigger>
|
onPaperSizeChange={setPaperSize}
|
||||||
<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>
|
|
||||||
|
|
||||||
<Link href={orderRoutes.create().url}>
|
<Link href={orderRoutes.create().url}>
|
||||||
<Button className="gap-2">
|
<Button className="gap-2">
|
||||||
|
|||||||
@ -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 { DataTableColumnHeader } from '@/components/data-table-column-header';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
import * as orderRoutes from '@/routes/order';
|
import * as orderRoutes from '@/routes/order';
|
||||||
import type { Order } from '@/types/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 {
|
interface ColumnProps {
|
||||||
onDelete: (order: Order) => void;
|
onDelete: (order: Order) => void;
|
||||||
@ -86,8 +86,8 @@ export const getColumns = ({ onDelete, onPrint }: ColumnProps): ColumnDef<Order>
|
|||||||
</span>
|
</span>
|
||||||
<Badge className="text-[10px] h-4 py-0" variant={
|
<Badge className="text-[10px] h-4 py-0" variant={
|
||||||
row.original.order_status === 'delivered' ? 'default' :
|
row.original.order_status === 'delivered' ? 'default' :
|
||||||
row.original.order_status === 'pending' ? 'secondary' :
|
row.original.order_status === 'pending' ? 'secondary' :
|
||||||
row.original.order_status === 'cancelled' ? 'destructive' : 'outline'
|
row.original.order_status === 'cancelled' ? 'destructive' : 'outline'
|
||||||
}>
|
}>
|
||||||
{row.original.order_status}
|
{row.original.order_status}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user