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) => ( + + ))} +
+ )} +
+
+
+ ) +}