feat: customer lookup from POS with order history and item search
- Customer dialog in cart panel: search accounts by name, phone, email, account # - Selected customer shown with name, phone, email in cart header - accountId passed when creating transactions - Order history view: tap a transaction to expand and see line items - Item search in history (e.g. "strings") — filters orders containing that item - Backend: add accountId and itemSearch filters to transaction list endpoint - itemSearch uses EXISTS subquery on line item descriptions (ILIKE) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -151,7 +151,7 @@ export function discountListOptions() {
|
|||||||
// --- Mutations ---
|
// --- Mutations ---
|
||||||
|
|
||||||
export const posMutations = {
|
export const posMutations = {
|
||||||
createTransaction: (data: { transactionType: string; locationId?: string }) =>
|
createTransaction: (data: { transactionType: string; locationId?: string; accountId?: string }) =>
|
||||||
api.post<Transaction>('/v1/transactions', data),
|
api.post<Transaction>('/v1/transactions', data),
|
||||||
|
|
||||||
addLineItem: (txnId: string, data: { productId?: string; inventoryUnitId?: string; description: string; qty: number; unitPrice: number }) =>
|
addLineItem: (txnId: string, data: { productId?: string; inventoryUnitId?: string; description: string; qty: number; unitPrice: number }) =>
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { usePOSStore } from '@/stores/pos.store'
|
|||||||
import { posMutations, posKeys, type Transaction } from '@/api/pos'
|
import { posMutations, posKeys, type Transaction } from '@/api/pos'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { X, Banknote, CreditCard, FileText, Ban } from 'lucide-react'
|
import { X, Banknote, CreditCard, FileText, Ban, UserRound } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { POSPaymentDialog } from './pos-payment-dialog'
|
import { POSPaymentDialog } from './pos-payment-dialog'
|
||||||
|
import { POSCustomerDialog } from './pos-customer-dialog'
|
||||||
|
|
||||||
interface POSCartPanelProps {
|
interface POSCartPanelProps {
|
||||||
transaction: Transaction | null
|
transaction: Transaction | null
|
||||||
@@ -14,8 +15,9 @@ interface POSCartPanelProps {
|
|||||||
|
|
||||||
export function POSCartPanel({ transaction }: POSCartPanelProps) {
|
export function POSCartPanel({ transaction }: POSCartPanelProps) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { currentTransactionId, setTransaction } = usePOSStore()
|
const { currentTransactionId, setTransaction, accountName, accountPhone, accountEmail } = usePOSStore()
|
||||||
const [paymentMethod, setPaymentMethod] = useState<string | null>(null)
|
const [paymentMethod, setPaymentMethod] = useState<string | null>(null)
|
||||||
|
const [customerOpen, setCustomerOpen] = useState(false)
|
||||||
const lineItems = transaction?.lineItems ?? []
|
const lineItems = transaction?.lineItems ?? []
|
||||||
|
|
||||||
const drawerSessionId = usePOSStore((s) => s.drawerSessionId)
|
const drawerSessionId = usePOSStore((s) => s.drawerSessionId)
|
||||||
@@ -63,6 +65,24 @@ export function POSCartPanel({ transaction }: POSCartPanelProps) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setCustomerOpen(true)}
|
||||||
|
className="flex items-start gap-1.5 mt-1 text-xs text-muted-foreground hover:text-foreground text-left"
|
||||||
|
>
|
||||||
|
<UserRound className="h-3 w-3 mt-0.5 shrink-0" />
|
||||||
|
{accountName ? (
|
||||||
|
<span>
|
||||||
|
<span className="font-medium text-foreground">{accountName}</span>
|
||||||
|
{(accountPhone || accountEmail) && (
|
||||||
|
<span className="block text-[11px]">
|
||||||
|
{[accountPhone, accountEmail].filter(Boolean).join(' · ')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>Walk-in — tap to add customer</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Line items */}
|
{/* Line items */}
|
||||||
@@ -182,6 +202,9 @@ export function POSCartPanel({ transaction }: POSCartPanelProps) {
|
|||||||
onComplete={handlePaymentComplete}
|
onComplete={handlePaymentComplete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Customer dialog */}
|
||||||
|
<POSCustomerDialog open={customerOpen} onOpenChange={setCustomerOpen} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
254
packages/admin/src/components/pos/pos-customer-dialog.tsx
Normal file
254
packages/admin/src/components/pos/pos-customer-dialog.tsx
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import { useState, useCallback } from 'react'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { queryOptions } from '@tanstack/react-query'
|
||||||
|
import { api } from '@/lib/api-client'
|
||||||
|
import { usePOSStore } from '@/stores/pos.store'
|
||||||
|
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 { Separator } from '@/components/ui/separator'
|
||||||
|
import { Search, X, History } from 'lucide-react'
|
||||||
|
|
||||||
|
interface Account {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
email: string | null
|
||||||
|
phone: string | null
|
||||||
|
accountNumber: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionLineItem {
|
||||||
|
id: string
|
||||||
|
description: string
|
||||||
|
qty: number
|
||||||
|
unitPrice: string
|
||||||
|
lineTotal: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Transaction {
|
||||||
|
id: string
|
||||||
|
transactionNumber: string
|
||||||
|
total: string
|
||||||
|
status: string
|
||||||
|
paymentMethod: string | null
|
||||||
|
transactionType: string
|
||||||
|
completedAt: string | null
|
||||||
|
createdAt: string
|
||||||
|
lineItems?: TransactionLineItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function accountSearchOptions(search: string) {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ['pos', 'accounts', search],
|
||||||
|
queryFn: () => api.get<{ data: Account[] }>('/v1/accounts', { q: search, limit: 10 }),
|
||||||
|
enabled: search.length >= 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function customerHistoryOptions(accountId: string | null) {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ['pos', 'customer-history', accountId],
|
||||||
|
queryFn: () => api.get<{ data: Transaction[] }>('/v1/transactions', {
|
||||||
|
accountId,
|
||||||
|
limit: 10,
|
||||||
|
sort: 'created_at',
|
||||||
|
order: 'desc',
|
||||||
|
...(historySearch ? { itemSearch: historySearch } : {}),
|
||||||
|
}),
|
||||||
|
enabled: !!accountId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface POSCustomerDialogProps {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function POSCustomerDialog({ open, onOpenChange }: POSCustomerDialogProps) {
|
||||||
|
const { accountId, accountName, setAccount, clearAccount } = usePOSStore()
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [showHistory, setShowHistory] = useState(false)
|
||||||
|
const [historySearch, setHistorySearch] = useState('')
|
||||||
|
|
||||||
|
const { data: searchData, isLoading } = useQuery(accountSearchOptions(search))
|
||||||
|
const accounts = searchData?.data ?? []
|
||||||
|
|
||||||
|
const { data: historyData } = useQuery(customerHistoryOptions(showHistory ? accountId : null))
|
||||||
|
const history = historyData?.data ?? []
|
||||||
|
|
||||||
|
function handleSelect(account: Account) {
|
||||||
|
setAccount(account.id, account.name, account.phone, account.email)
|
||||||
|
setSearch('')
|
||||||
|
setShowHistory(false)
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
clearAccount()
|
||||||
|
setSearch('')
|
||||||
|
setShowHistory(false)
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [expandedTxn, setExpandedTxn] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// Fetch detail for expanded transaction
|
||||||
|
const { data: txnDetail } = useQuery({
|
||||||
|
queryKey: ['pos', 'transaction-detail', expandedTxn],
|
||||||
|
queryFn: () => api.get<Transaction>(`/v1/transactions/${expandedTxn}`),
|
||||||
|
enabled: !!expandedTxn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleExpand = useCallback((id: string) => {
|
||||||
|
setExpandedTxn((prev) => prev === id ? null : id)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// History view
|
||||||
|
if (showHistory && accountId) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-md max-h-[80vh] flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center justify-between">
|
||||||
|
<span>Order History — {accountName}</span>
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => { setShowHistory(false); setExpandedTxn(null) }}>Back</Button>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{/* Search items in history */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
value={historySearch}
|
||||||
|
onChange={(e) => setHistorySearch(e.target.value)}
|
||||||
|
placeholder="Search items (e.g. strings, bow)..."
|
||||||
|
className="pl-10 h-10 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{history.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground text-center py-8">
|
||||||
|
{historySearch ? `No orders with "${historySearch}"` : 'No transactions found'}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-border">
|
||||||
|
{history.map((txn) => (
|
||||||
|
<div key={txn.id}>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleExpand(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' : '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).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{expandedTxn === txn.id && txnDetail?.lineItems && (
|
||||||
|
<div className="px-3 pb-2 space-y-1">
|
||||||
|
{txnDetail.lineItems.map((item) => (
|
||||||
|
<div key={item.id} className="flex justify-between text-xs text-muted-foreground">
|
||||||
|
<span>{item.qty} x {item.description}</span>
|
||||||
|
<span>${parseFloat(item.lineTotal).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-md max-h-[80vh] flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Customer</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Current selection */}
|
||||||
|
{accountId && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between p-3 rounded-md border bg-muted/30">
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm">{accountName}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Selected customer</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => setShowHistory(true)}>
|
||||||
|
<History className="h-4 w-4 mr-1" />
|
||||||
|
History
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={handleClear}>
|
||||||
|
<X className="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Search */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
placeholder="Search by name, phone, email, account #..."
|
||||||
|
className="pl-10 h-11"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Results */}
|
||||||
|
<div className="flex-1 overflow-y-auto">
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="text-sm text-muted-foreground text-center py-4">Searching...</p>
|
||||||
|
) : search.length >= 2 && accounts.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground text-center py-4">No customers found</p>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-border">
|
||||||
|
{accounts.map((account) => (
|
||||||
|
<button
|
||||||
|
key={account.id}
|
||||||
|
onClick={() => handleSelect(account)}
|
||||||
|
className="w-full text-left px-2 py-3 hover:bg-accent rounded-md transition-colors"
|
||||||
|
>
|
||||||
|
<p className="font-medium text-sm">{account.name}</p>
|
||||||
|
<div className="flex gap-3 text-xs text-muted-foreground mt-0.5">
|
||||||
|
{account.phone && <span>{account.phone}</span>}
|
||||||
|
{account.email && <span>{account.email}</span>}
|
||||||
|
{account.accountNumber && <span>#{account.accountNumber}</span>}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Walk-in button */}
|
||||||
|
{accountId && (
|
||||||
|
<Button variant="outline" className="w-full h-11" onClick={handleClear}>
|
||||||
|
Clear Customer (Walk-in)
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ interface POSItemPanelProps {
|
|||||||
|
|
||||||
export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) {
|
export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const { currentTransactionId, setTransaction, locationId } = 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 [customDesc, setCustomDesc] = useState('')
|
const [customDesc, setCustomDesc] = useState('')
|
||||||
@@ -40,6 +40,7 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) {
|
|||||||
const txn = await posMutations.createTransaction({
|
const txn = await posMutations.createTransaction({
|
||||||
transactionType: 'sale',
|
transactionType: 'sale',
|
||||||
locationId: locationId ?? undefined,
|
locationId: locationId ?? undefined,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
})
|
})
|
||||||
txnId = txn.id
|
txnId = txn.id
|
||||||
setTransaction(txnId)
|
setTransaction(txnId)
|
||||||
@@ -66,6 +67,7 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) {
|
|||||||
const txn = await posMutations.createTransaction({
|
const txn = await posMutations.createTransaction({
|
||||||
transactionType: 'sale',
|
transactionType: 'sale',
|
||||||
locationId: locationId ?? undefined,
|
locationId: locationId ?? undefined,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
})
|
})
|
||||||
txnId = txn.id
|
txnId = txn.id
|
||||||
setTransaction(txnId)
|
setTransaction(txnId)
|
||||||
@@ -96,6 +98,7 @@ export function POSItemPanel({ transaction: _transaction }: POSItemPanelProps) {
|
|||||||
const txn = await posMutations.createTransaction({
|
const txn = await posMutations.createTransaction({
|
||||||
transactionType: 'sale',
|
transactionType: 'sale',
|
||||||
locationId: locationId ?? undefined,
|
locationId: locationId ?? undefined,
|
||||||
|
accountId: accountId ?? undefined,
|
||||||
})
|
})
|
||||||
txnId = txn.id
|
txnId = txn.id
|
||||||
setTransaction(txnId)
|
setTransaction(txnId)
|
||||||
|
|||||||
@@ -16,12 +16,18 @@ interface POSState {
|
|||||||
cashier: POSUser | null
|
cashier: POSUser | null
|
||||||
token: string | null
|
token: string | null
|
||||||
lastActivity: number
|
lastActivity: number
|
||||||
|
accountId: string | null
|
||||||
|
accountName: string | null
|
||||||
|
accountPhone: string | null
|
||||||
|
accountEmail: string | null
|
||||||
setTransaction: (id: string | null) => void
|
setTransaction: (id: string | null) => void
|
||||||
setLocation: (id: string) => void
|
setLocation: (id: string) => void
|
||||||
setDrawerSession: (id: string | null) => void
|
setDrawerSession: (id: string | null) => void
|
||||||
unlock: (user: POSUser, token: string) => void
|
unlock: (user: POSUser, token: string) => void
|
||||||
lock: () => void
|
lock: () => void
|
||||||
touchActivity: () => void
|
touchActivity: () => void
|
||||||
|
setAccount: (id: string, name: string, phone?: string | null, email?: string | null) => void
|
||||||
|
clearAccount: () => void
|
||||||
reset: () => void
|
reset: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,11 +39,17 @@ export const usePOSStore = create<POSState>((set) => ({
|
|||||||
cashier: null,
|
cashier: null,
|
||||||
token: null,
|
token: null,
|
||||||
lastActivity: Date.now(),
|
lastActivity: Date.now(),
|
||||||
|
accountId: null,
|
||||||
|
accountName: null,
|
||||||
|
accountPhone: null,
|
||||||
|
accountEmail: null,
|
||||||
setTransaction: (id) => set({ currentTransactionId: id }),
|
setTransaction: (id) => set({ currentTransactionId: id }),
|
||||||
setLocation: (id) => set({ locationId: id }),
|
setLocation: (id) => set({ locationId: id }),
|
||||||
setDrawerSession: (id) => set({ drawerSessionId: id }),
|
setDrawerSession: (id) => set({ drawerSessionId: id }),
|
||||||
unlock: (user, token) => set({ locked: false, cashier: user, token, lastActivity: Date.now() }),
|
unlock: (user, token) => set({ locked: false, cashier: user, token, lastActivity: Date.now() }),
|
||||||
lock: () => set({ locked: true, currentTransactionId: null }),
|
lock: () => set({ locked: true, currentTransactionId: null }),
|
||||||
touchActivity: () => set({ lastActivity: Date.now() }),
|
touchActivity: () => set({ lastActivity: Date.now() }),
|
||||||
reset: () => set({ currentTransactionId: null }),
|
setAccount: (id, name, phone, email) => set({ accountId: id, accountName: name, accountPhone: phone ?? null, accountEmail: email ?? null }),
|
||||||
|
clearAccount: () => set({ accountId: null, accountName: null, accountPhone: null, accountEmail: null }),
|
||||||
|
reset: () => set({ currentTransactionId: null, accountId: null, accountName: null, accountPhone: null, accountEmail: null }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export const transactionRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
status: query.status,
|
status: query.status,
|
||||||
transactionType: query.transactionType,
|
transactionType: query.transactionType,
|
||||||
locationId: query.locationId,
|
locationId: query.locationId,
|
||||||
|
accountId: query.accountId,
|
||||||
|
itemSearch: query.itemSearch,
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await TransactionService.list(app.db, params, filters)
|
const result = await TransactionService.list(app.db, params, filters)
|
||||||
|
|||||||
@@ -390,12 +390,20 @@ export const TransactionService = {
|
|||||||
status?: string
|
status?: string
|
||||||
transactionType?: string
|
transactionType?: string
|
||||||
locationId?: string
|
locationId?: string
|
||||||
|
accountId?: string
|
||||||
|
itemSearch?: string
|
||||||
}) {
|
}) {
|
||||||
const conditions: ReturnType<typeof eq>[] = []
|
const conditions: ReturnType<typeof eq>[] = []
|
||||||
|
|
||||||
if (params.q) {
|
if (params.q) {
|
||||||
conditions.push(buildSearchCondition(params.q, [transactions.transactionNumber])!)
|
conditions.push(buildSearchCondition(params.q, [transactions.transactionNumber])!)
|
||||||
}
|
}
|
||||||
|
if (filters?.itemSearch) {
|
||||||
|
const term = `%${filters.itemSearch}%`
|
||||||
|
conditions.push(
|
||||||
|
sql`EXISTS (SELECT 1 FROM ${transactionLineItems} WHERE ${transactionLineItems.transactionId} = ${transactions.id} AND ${transactionLineItems.description} ILIKE ${term})`
|
||||||
|
)
|
||||||
|
}
|
||||||
if (filters?.status) {
|
if (filters?.status) {
|
||||||
conditions.push(eq(transactions.status, filters.status as any))
|
conditions.push(eq(transactions.status, filters.status as any))
|
||||||
}
|
}
|
||||||
@@ -405,6 +413,9 @@ export const TransactionService = {
|
|||||||
if (filters?.locationId) {
|
if (filters?.locationId) {
|
||||||
conditions.push(eq(transactions.locationId, filters.locationId))
|
conditions.push(eq(transactions.locationId, filters.locationId))
|
||||||
}
|
}
|
||||||
|
if (filters?.accountId) {
|
||||||
|
conditions.push(eq(transactions.accountId, filters.accountId))
|
||||||
|
}
|
||||||
|
|
||||||
const where = conditions.length === 0 ? undefined : conditions.length === 1 ? conditions[0] : and(...conditions)
|
const where = conditions.length === 0 ? undefined : conditions.length === 1 ? conditions[0] : and(...conditions)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user