From a48da03289de0b1ddc21452bb12447af93d8f29b Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 4 Apr 2026 23:38:03 +0000 Subject: [PATCH] feat: orders lookup with receipt reprint, refresh stock after sale - "Orders" button in POS quick actions shows recent transactions - Search by transaction number, tap to view receipt, print or save PDF - Product stock counts refresh after completing a sale - Invalidate product search queries on payment completion Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/pos/pos-item-panel.tsx | 15 +- .../src/components/pos/pos-payment-dialog.tsx | 1 + .../pos/pos-transactions-dialog.tsx | 143 ++++++++++++++++++ 3 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 packages/admin/src/components/pos/pos-transactions-dialog.tsx diff --git a/packages/admin/src/components/pos/pos-item-panel.tsx b/packages/admin/src/components/pos/pos-item-panel.tsx index 0cdf94d..352aac3 100644 --- a/packages/admin/src/components/pos/pos-item-panel.tsx +++ b/packages/admin/src/components/pos/pos-item-panel.tsx @@ -7,8 +7,9 @@ import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Label } from '@/components/ui/label' import { Skeleton } from '@/components/ui/skeleton' -import { Search, ScanBarcode, Wrench, PenLine } from 'lucide-react' +import { Search, ScanBarcode, Wrench, PenLine, ClipboardList } from 'lucide-react' import { toast } from 'sonner' +import { POSTransactionsDialog } from './pos-transactions-dialog' interface POSItemPanelProps { transaction: Transaction | null @@ -19,6 +20,7 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) { const { currentTransactionId, setTransaction, locationId, accountId } = usePOSStore() const [search, setSearch] = useState('') const [customOpen, setCustomOpen] = useState(false) + const [txnDialogOpen, setTxnDialogOpen] = useState(false) const [customDesc, setCustomDesc] = useState('') const [customPrice, setCustomPrice] = useState('') const [customQty, setCustomQty] = useState('1') @@ -211,6 +213,14 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) { Custom + {/* Custom item dialog */} @@ -264,6 +274,9 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) { + + {/* Transactions dialog */} + ) } diff --git a/packages/admin/src/components/pos/pos-payment-dialog.tsx b/packages/admin/src/components/pos/pos-payment-dialog.tsx index 609e918..41194c8 100644 --- a/packages/admin/src/components/pos/pos-payment-dialog.tsx +++ b/packages/admin/src/components/pos/pos-payment-dialog.tsx @@ -44,6 +44,7 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio }, onSuccess: (txn) => { queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId!) }) + queryClient.invalidateQueries({ queryKey: ['pos', 'products'] }) setResult(txn) setCompleted(true) }, diff --git a/packages/admin/src/components/pos/pos-transactions-dialog.tsx b/packages/admin/src/components/pos/pos-transactions-dialog.tsx new file mode 100644 index 0000000..c08eee5 --- /dev/null +++ b/packages/admin/src/components/pos/pos-transactions-dialog.tsx @@ -0,0 +1,143 @@ +import { useState } from 'react' +import { useQuery } from '@tanstack/react-query' +import { queryOptions } from '@tanstack/react-query' +import { api } from '@/lib/api-client' +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { Printer } from 'lucide-react' +import { POSReceipt, printReceipt, downloadReceiptPDF } from './pos-receipt' +import type { Transaction } from '@/api/pos' + +interface ReceiptData { + transaction: Transaction & { lineItems: { description: string; qty: number; unitPrice: string; taxAmount: string; lineTotal: string; discountAmount: string }[] } + company: { name: string; phone: string | null; email: string | null; address: { street?: string; city?: string; state?: string; zip?: string } | null } + location: { name: string; phone: string | null; email: string | null; address: { street?: string; city?: string; state?: string; zip?: string } | null } +} + +interface AppConfigEntry { key: string; value: string | null } + +function recentTransactionsOptions(search: string) { + return queryOptions({ + queryKey: ['pos', 'recent-transactions', search], + queryFn: () => api.get<{ data: Transaction[] }>('/v1/transactions', { + limit: 15, + sort: 'created_at', + order: 'desc', + ...(search ? { q: search } : {}), + }), + }) +} + +interface POSTransactionsDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function POSTransactionsDialog({ open, onOpenChange }: POSTransactionsDialogProps) { + const [search, setSearch] = useState('') + const [receiptTxnId, setReceiptTxnId] = useState(null) + + const { data: txnData } = useQuery({ + ...recentTransactionsOptions(search), + enabled: open, + }) + const transactions = txnData?.data ?? [] + + // Fetch receipt for selected transaction + const { data: receiptData } = useQuery({ + queryKey: ['pos', 'receipt', receiptTxnId], + queryFn: () => api.get(`/v1/transactions/${receiptTxnId}/receipt`), + enabled: !!receiptTxnId, + }) + + const { data: configData } = useQuery({ + queryKey: ['config'], + queryFn: () => api.get<{ data: AppConfigEntry[] }>('/v1/config'), + enabled: !!receiptTxnId, + }) + const receiptConfig = { + header: configData?.data?.find((c) => c.key === 'receipt_header')?.value || undefined, + footer: configData?.data?.find((c) => c.key === 'receipt_footer')?.value || undefined, + returnPolicy: configData?.data?.find((c) => c.key === 'receipt_return_policy')?.value || undefined, + social: configData?.data?.find((c) => c.key === 'receipt_social')?.value || undefined, + } + + // Receipt view + if (receiptTxnId && receiptData) { + return ( + + +
+ +
+ + +
+
+
+ +
+
+
+ ) + } + + return ( + + + + Recent Transactions + + + setSearch(e.target.value)} + placeholder="Search by transaction number..." + className="h-10" + autoFocus + /> + +
+ {transactions.length === 0 ? ( +

No transactions found

+ ) : ( +
+ {transactions.map((txn) => ( + + ))} +
+ )} +
+
+
+ ) +}