Files
lunarfront-app/packages/admin/src/api/pos.ts
ryan d21972212b 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>
2026-04-05 16:05:19 +00:00

187 lines
5.3 KiB
TypeScript

import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
// --- Types ---
export interface Transaction {
id: string
locationId: string | null
transactionNumber: string
accountId: string | null
repairTicketId: string | null
repairBatchId: string | null
transactionType: string
status: string
subtotal: string
discountTotal: string
taxTotal: string
total: string
paymentMethod: string | null
amountTendered: string | null
changeGiven: string | null
checkNumber: string | null
roundingAdjustment: string
taxExempt: boolean
taxExemptReason: string | null
processedBy: string
drawerSessionId: string | null
notes: string | null
completedAt: string | null
createdAt: string
updatedAt: string
lineItems?: TransactionLineItem[]
}
export interface TransactionLineItem {
id: string
transactionId: string
productId: string | null
inventoryUnitId: string | null
description: string
qty: number
unitPrice: string
discountAmount: string
discountReason: string | null
taxRate: string
taxAmount: string
lineTotal: string
createdAt: string
}
export interface DrawerSession {
id: string
locationId: string | null
openedBy: string
closedBy: string | null
openingBalance: string
closingBalance: string | null
expectedBalance: string | null
overShort: string | null
denominations: Record<string, number> | null
status: string
notes: string | null
openedAt: string
closedAt: string | null
}
export interface Discount {
id: string
name: string
discountType: string
discountValue: string
appliesTo: string
requiresApprovalAbove: string | null
isActive: boolean
}
export interface Product {
id: string
name: string
sku: string | null
upc: string | null
description: string | null
price: string | null
costPrice: string | null
qtyOnHand: number | null
taxCategory: string
isSerialized: boolean
isActive: boolean
}
// --- Query Keys ---
export interface DrawerAdjustment {
id: string
drawerSessionId: string
type: string
amount: string
reason: string
createdBy: string
createdAt: string
}
export const posKeys = {
transaction: (id: string) => ['pos', 'transaction', id] as const,
drawer: (locationId: string) => ['pos', 'drawer', locationId] as const,
drawerAdjustments: (id: string) => ['pos', 'drawer-adjustments', id] as const,
products: (search: string) => ['pos', 'products', search] as const,
discounts: ['pos', 'discounts'] as const,
}
// --- Query Options ---
export function transactionOptions(id: string | null) {
return queryOptions({
queryKey: posKeys.transaction(id ?? ''),
queryFn: () => api.get<Transaction>(`/v1/transactions/${id}`),
enabled: !!id,
})
}
export function currentDrawerOptions(locationId: string | null) {
return queryOptions({
queryKey: posKeys.drawer(locationId ?? ''),
queryFn: async (): Promise<DrawerSession | null> => {
try {
return await api.get<DrawerSession>('/v1/drawer/current', { locationId })
} catch {
return null // 404 = no open drawer
}
},
enabled: !!locationId,
retry: false,
})
}
export function productSearchOptions(search: string) {
return queryOptions({
queryKey: posKeys.products(search),
queryFn: () => api.get<{ data: Product[]; pagination: { page: number; limit: number; total: number; totalPages: number } }>('/v1/products', { q: search, limit: 24, isActive: true }),
enabled: search.length >= 1,
})
}
export function discountListOptions() {
return queryOptions({
queryKey: posKeys.discounts,
queryFn: () => api.get<Discount[]>('/v1/discounts/all'),
})
}
// --- Mutations ---
export const posMutations = {
createTransaction: (data: { transactionType: string; locationId?: string; accountId?: string }) =>
api.post<Transaction>('/v1/transactions', data),
addLineItem: (txnId: string, data: { productId?: string; inventoryUnitId?: string; description: string; qty: number; unitPrice: number }) =>
api.post<TransactionLineItem>(`/v1/transactions/${txnId}/line-items`, data),
removeLineItem: (txnId: string, lineItemId: string) =>
api.del<TransactionLineItem>(`/v1/transactions/${txnId}/line-items/${lineItemId}`),
applyDiscount: (txnId: string, data: { discountId?: string; amount: number; reason: string; lineItemId?: string }) =>
api.post<Transaction>(`/v1/transactions/${txnId}/discounts`, data),
complete: (txnId: string, data: { paymentMethod: string; amountTendered?: number; checkNumber?: string }) =>
api.post<Transaction>(`/v1/transactions/${txnId}/complete`, data),
void: (txnId: string) =>
api.post<Transaction>(`/v1/transactions/${txnId}/void`, {}),
openDrawer: (data: { locationId?: string; openingBalance: number }) =>
api.post<DrawerSession>('/v1/drawer/open', data),
closeDrawer: (id: string, data: { closingBalance: number; denominations?: Record<string, number>; notes?: string }) =>
api.post<DrawerSession>(`/v1/drawer/${id}/close`, data),
lookupUpc: (upc: string) =>
api.get<Product>(`/v1/products/lookup/upc/${upc}`),
addAdjustment: (drawerId: string, data: { type: string; amount: number; reason: string }) =>
api.post<DrawerAdjustment>(`/v1/drawer/${drawerId}/adjustments`, data),
getAdjustments: (drawerId: string) =>
api.get<{ data: DrawerAdjustment[] }>(`/v1/drawer/${drawerId}/adjustments`),
}