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 { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
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 { toast } from 'sonner'
|
||||||
|
import { POSTransactionsDialog } from './pos-transactions-dialog'
|
||||||
|
|
||||||
interface POSItemPanelProps {
|
interface POSItemPanelProps {
|
||||||
transaction: Transaction | null
|
transaction: Transaction | null
|
||||||
@@ -19,6 +20,7 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) {
|
|||||||
const { currentTransactionId, setTransaction, locationId, accountId } = usePOSStore()
|
const { currentTransactionId, setTransaction, locationId, accountId } = usePOSStore()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
const [customOpen, setCustomOpen] = useState(false)
|
const [customOpen, setCustomOpen] = useState(false)
|
||||||
|
const [txnDialogOpen, setTxnDialogOpen] = useState(false)
|
||||||
const [customDesc, setCustomDesc] = useState('')
|
const [customDesc, setCustomDesc] = useState('')
|
||||||
const [customPrice, setCustomPrice] = useState('')
|
const [customPrice, setCustomPrice] = useState('')
|
||||||
const [customQty, setCustomQty] = useState('1')
|
const [customQty, setCustomQty] = useState('1')
|
||||||
@@ -211,6 +213,14 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) {
|
|||||||
<PenLine className="h-4 w-4" />
|
<PenLine className="h-4 w-4" />
|
||||||
Custom
|
Custom
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Custom item dialog */}
|
{/* Custom item dialog */}
|
||||||
@@ -264,6 +274,9 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) {
|
|||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Transactions dialog */}
|
||||||
|
<POSTransactionsDialog open={txnDialogOpen} onOpenChange={setTxnDialogOpen} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio
|
|||||||
},
|
},
|
||||||
onSuccess: (txn) => {
|
onSuccess: (txn) => {
|
||||||
queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId!) })
|
queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId!) })
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['pos', 'products'] })
|
||||||
setResult(txn)
|
setResult(txn)
|
||||||
setCompleted(true)
|
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