feat: printable receipts with barcode on payment complete
- Receipt component with thermal (80mm) and full-page layout support - Code 128 barcode from transaction number via JsBarcode - Store name, address, line items, totals, payment info, barcode - Print button on sale complete screen (browser print dialog) - Email button placeholder (disabled, ready for SMTP integration) - @media print CSS hides everything except receipt content - Receipt data fetched from GET /transactions/:id/receipt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
import { useState } from 'react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { usePOSStore } from '@/stores/pos.store'
|
||||
import { api } from '@/lib/api-client'
|
||||
import { posMutations, posKeys, type Transaction } from '@/api/pos'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { CheckCircle } from 'lucide-react'
|
||||
import { CheckCircle, Printer, Mail } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { POSReceipt, printReceipt } from './pos-receipt'
|
||||
|
||||
interface POSPaymentDialogProps {
|
||||
open: boolean
|
||||
@@ -61,9 +63,40 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio
|
||||
|
||||
const QUICK_AMOUNTS = [1, 5, 10, 20, 50, 100]
|
||||
|
||||
// Fetch full receipt data after completion
|
||||
const { data: receiptData } = useQuery({
|
||||
queryKey: ['pos', 'receipt', result?.id],
|
||||
queryFn: () => api.get<{
|
||||
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 }
|
||||
}>(`/v1/transactions/${result!.id}/receipt`),
|
||||
enabled: !!result?.id,
|
||||
})
|
||||
|
||||
const [showReceipt, setShowReceipt] = useState(false)
|
||||
|
||||
if (completed && result) {
|
||||
const changeGiven = parseFloat(result.changeGiven ?? '0')
|
||||
const roundingAdj = parseFloat(result.roundingAdjustment ?? '0')
|
||||
|
||||
// Receipt print view
|
||||
if (showReceipt && receiptData) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={() => { setShowReceipt(false); handleDone() }}>
|
||||
<DialogContent className="max-w-sm max-h-[90vh] overflow-y-auto print:max-w-none print:max-h-none print:overflow-visible print:shadow-none print:border-none">
|
||||
<div className="print:hidden flex justify-between items-center mb-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowReceipt(false)}>Back</Button>
|
||||
<Button size="sm" onClick={printReceipt} className="gap-2">
|
||||
<Printer className="h-4 w-4" />Print
|
||||
</Button>
|
||||
</div>
|
||||
<div className="print:block">
|
||||
<POSReceipt data={receiptData} footerText="Thank you for your business!" />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={() => handleDone()}>
|
||||
@@ -78,26 +111,21 @@ export function POSPaymentDialog({ open, onOpenChange, paymentMethod, transactio
|
||||
<span>Total</span>
|
||||
<span>${parseFloat(result.total).toFixed(2)}</span>
|
||||
</div>
|
||||
{roundingAdj !== 0 && (
|
||||
<div className="flex justify-between text-muted-foreground">
|
||||
<span>Rounding</span>
|
||||
<span>{roundingAdj > 0 ? '+' : ''}{roundingAdj.toFixed(2)}</span>
|
||||
{paymentMethod === 'cash' && changeGiven > 0 && (
|
||||
<div className="flex justify-between text-lg font-bold text-green-600">
|
||||
<span>Change Due</span>
|
||||
<span>${changeGiven.toFixed(2)}</span>
|
||||
</div>
|
||||
)}
|
||||
{paymentMethod === 'cash' && (
|
||||
<>
|
||||
<div className="flex justify-between">
|
||||
<span>Tendered</span>
|
||||
<span>${parseFloat(result.amountTendered ?? '0').toFixed(2)}</span>
|
||||
</div>
|
||||
{changeGiven > 0 && (
|
||||
<div className="flex justify-between text-lg font-bold text-green-600">
|
||||
<span>Change Due</span>
|
||||
<span>${changeGiven.toFixed(2)}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="w-full grid grid-cols-2 gap-2">
|
||||
<Button variant="outline" className="h-11 gap-2" onClick={() => setShowReceipt(true)}>
|
||||
<Printer className="h-4 w-4" />Receipt
|
||||
</Button>
|
||||
<Button variant="outline" className="h-11 gap-2" disabled>
|
||||
<Mail className="h-4 w-4" />Email
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button className="w-full h-12 text-base" onClick={handleDone}>
|
||||
|
||||
Reference in New Issue
Block a user