fix: require open drawer to complete transactions, fix product price field
- Backend enforces open drawer at location before completing any transaction - Frontend disables payment buttons when drawer is closed with warning message - Fix product price field name (price, not sellingPrice) in POS API types - Fix seed UUIDs to use valid UUID v4 format (version nibble must be 1-8) - Fix Vite allowedHosts for dev.lunarfront.tech access - Add e2e test for drawer enforcement (39 POS tests now pass) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -80,7 +80,7 @@ export interface Product {
|
||||
sku: string | null
|
||||
upc: string | null
|
||||
description: string | null
|
||||
sellingPrice: string | null
|
||||
price: string | null
|
||||
costPrice: string | null
|
||||
qtyOnHand: number | null
|
||||
taxCategory: string
|
||||
|
||||
@@ -18,6 +18,9 @@ export function POSCartPanel({ transaction }: POSCartPanelProps) {
|
||||
const [paymentMethod, setPaymentMethod] = useState<string | null>(null)
|
||||
const lineItems = transaction?.lineItems ?? []
|
||||
|
||||
const drawerSessionId = usePOSStore((s) => s.drawerSessionId)
|
||||
const drawerOpen = !!drawerSessionId
|
||||
|
||||
const removeItemMutation = useMutation({
|
||||
mutationFn: (lineItemId: string) =>
|
||||
posMutations.removeLineItem(currentTransactionId!, lineItemId),
|
||||
@@ -126,41 +129,46 @@ export function POSCartPanel({ transaction }: POSCartPanelProps) {
|
||||
</div>
|
||||
|
||||
{/* Payment buttons */}
|
||||
<div className="p-3 grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
className="h-12 text-sm gap-2"
|
||||
disabled={!hasItems || !isPending}
|
||||
onClick={() => setPaymentMethod('cash')}
|
||||
>
|
||||
<Banknote className="h-4 w-4" />
|
||||
Cash
|
||||
</Button>
|
||||
<Button
|
||||
className="h-12 text-sm gap-2"
|
||||
disabled={!hasItems || !isPending}
|
||||
onClick={() => setPaymentMethod('card_present')}
|
||||
>
|
||||
<CreditCard className="h-4 w-4" />
|
||||
Card
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-12 text-sm gap-2"
|
||||
disabled={!hasItems || !isPending}
|
||||
onClick={() => setPaymentMethod('check')}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Check
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="h-12 text-sm gap-2"
|
||||
disabled={!hasItems || !isPending}
|
||||
onClick={() => voidMutation.mutate()}
|
||||
>
|
||||
<Ban className="h-4 w-4" />
|
||||
Void
|
||||
</Button>
|
||||
<div className="p-3 space-y-2">
|
||||
{!drawerOpen && hasItems && (
|
||||
<p className="text-xs text-destructive text-center">Open the drawer before accepting payment</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
className="h-12 text-sm gap-2"
|
||||
disabled={!hasItems || !isPending || !drawerOpen}
|
||||
onClick={() => setPaymentMethod('cash')}
|
||||
>
|
||||
<Banknote className="h-4 w-4" />
|
||||
Cash
|
||||
</Button>
|
||||
<Button
|
||||
className="h-12 text-sm gap-2"
|
||||
disabled={!hasItems || !isPending || !drawerOpen}
|
||||
onClick={() => setPaymentMethod('card_present')}
|
||||
>
|
||||
<CreditCard className="h-4 w-4" />
|
||||
Card
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-12 text-sm gap-2"
|
||||
disabled={!hasItems || !isPending || !drawerOpen}
|
||||
onClick={() => setPaymentMethod('check')}
|
||||
>
|
||||
<FileText className="h-4 w-4" />
|
||||
Check
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="h-12 text-sm gap-2"
|
||||
disabled={!hasItems || !isPending}
|
||||
onClick={() => voidMutation.mutate()}
|
||||
>
|
||||
<Ban className="h-4 w-4" />
|
||||
Void
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -48,11 +48,12 @@ export function POSItemPanel({ transaction }: POSItemPanelProps) {
|
||||
productId: product.id,
|
||||
description: product.name,
|
||||
qty: 1,
|
||||
unitPrice: parseFloat(product.sellingPrice ?? '0'),
|
||||
unitPrice: parseFloat(product.price ?? '0'),
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId ?? '') })
|
||||
const txnId = usePOSStore.getState().currentTransactionId
|
||||
queryClient.invalidateQueries({ queryKey: posKeys.transaction(txnId ?? '') })
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
@@ -76,7 +77,8 @@ export function POSItemPanel({ transaction }: POSItemPanelProps) {
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: posKeys.transaction(currentTransactionId ?? '') })
|
||||
const txnId = usePOSStore.getState().currentTransactionId
|
||||
queryClient.invalidateQueries({ queryKey: posKeys.transaction(txnId ?? '') })
|
||||
setCustomOpen(false)
|
||||
setCustomDesc('')
|
||||
setCustomPrice('')
|
||||
@@ -102,7 +104,7 @@ export function POSItemPanel({ transaction }: POSItemPanelProps) {
|
||||
productId: product.id,
|
||||
description: product.name,
|
||||
qty: 1,
|
||||
unitPrice: parseFloat(product.sellingPrice ?? '0'),
|
||||
unitPrice: parseFloat(product.price ?? '0'),
|
||||
})
|
||||
},
|
||||
onSuccess: () => {
|
||||
@@ -158,7 +160,7 @@ export function POSItemPanel({ transaction }: POSItemPanelProps) {
|
||||
>
|
||||
<span className="font-medium text-sm line-clamp-2">{product.name}</span>
|
||||
<div className="mt-auto flex items-center justify-between w-full pt-1">
|
||||
<span className="text-base font-semibold">${parseFloat(product.sellingPrice ?? '0').toFixed(2)}</span>
|
||||
<span className="text-base font-semibold">${parseFloat(product.price ?? '0').toFixed(2)}</span>
|
||||
{product.sku && <span className="text-xs text-muted-foreground">{product.sku}</span>}
|
||||
</div>
|
||||
{product.isSerialized ? (
|
||||
|
||||
@@ -17,6 +17,7 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
allowedHosts: ['dev.lunarfront.tech'],
|
||||
proxy: {
|
||||
'/v1': {
|
||||
target: 'http://localhost:8000',
|
||||
|
||||
Reference in New Issue
Block a user