- 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>
187 lines
5.3 KiB
TypeScript
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`),
|
|
}
|