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) <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
<PenLine className="h-4 w-4" />
|
||||
Custom
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-1 h-11 text-sm gap-2"
|
||||
onClick={() => setTxnDialogOpen(true)}
|
||||
>
|
||||
<ClipboardList className="h-4 w-4" />
|
||||
Orders
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Custom item dialog */}
|
||||
@@ -264,6 +274,9 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) {
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Transactions dialog */}
|
||||
<POSTransactionsDialog open={txnDialogOpen} onOpenChange={setTxnDialogOpen} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
|
||||
143
packages/admin/src/components/pos/pos-transactions-dialog.tsx
Normal file
143
packages/admin/src/components/pos/pos-transactions-dialog.tsx
Normal file
@@ -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<string | null>(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<ReceiptData>(`/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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-sm max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => setReceiptTxnId(null)}>Back</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => downloadReceiptPDF(receiptData.transaction.transactionNumber)}>
|
||||
Save PDF
|
||||
</Button>
|
||||
<Button size="sm" onClick={printReceipt} className="gap-2">
|
||||
<Printer className="h-4 w-4" />Print
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pos-receipt-print">
|
||||
<POSReceipt data={receiptData} config={receiptConfig} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md max-h-[80vh] flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Recent Transactions</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
placeholder="Search by transaction number..."
|
||||
className="h-10"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{transactions.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">No transactions found</p>
|
||||
) : (
|
||||
<div className="divide-y divide-border">
|
||||
{transactions.map((txn) => (
|
||||
<button
|
||||
key={txn.id}
|
||||
onClick={() => setReceiptTxnId(txn.id)}
|
||||
className="w-full text-left py-2.5 px-1 hover:bg-accent/50 rounded transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-mono">{txn.transactionNumber}</span>
|
||||
<span className="text-sm font-semibold">${parseFloat(txn.total).toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<Badge
|
||||
variant={txn.status === 'completed' ? 'default' : txn.status === 'voided' ? 'destructive' : 'outline'}
|
||||
className="text-[10px]"
|
||||
>
|
||||
{txn.status}
|
||||
</Badge>
|
||||
{txn.paymentMethod && (
|
||||
<span className="text-xs text-muted-foreground">{txn.paymentMethod.replace('_', ' ')}</span>
|
||||
)}
|
||||
<span className="text-xs text-muted-foreground ml-auto">
|
||||
{new Date(txn.completedAt ?? txn.createdAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user