Build inventory frontend and stock management features
- Full inventory UI: product list with search/filter, product detail with tabs (details, units, suppliers, stock receipts, price history) - Product filters: category, type (serialized/rental/repair), low stock, active/inactive — all server-side with URL-synced state - Product-supplier junction: link products to multiple suppliers with preferred flag, joined supplier details in UI - Stock receipts: record incoming stock with supplier, qty, cost per unit, invoice number; auto-increments qty_on_hand for non-serialized products - Price history tab on product detail page - categories/all endpoint to avoid pagination limit on dropdown fetches - categoryId filter on product list endpoint - Repair parts and additional inventory items in music store seed data - isDualUseRepair corrected: instruments set to false, strings/parts true - Product-supplier links and stock receipts in seed data - Price history seed data simulating cost increases over past year - 37 API tests covering categories, suppliers, products, units, product-suppliers, and stock receipts - alert-dialog and checkbox UI components - sync-and-deploy.sh script for rsync + remote deploy
This commit is contained in:
162
packages/admin/src/api/inventory.ts
Normal file
162
packages/admin/src/api/inventory.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { queryOptions } from '@tanstack/react-query'
|
||||
import { api } from '@/lib/api-client'
|
||||
import type { Category, Supplier, Product, InventoryUnit, ProductSupplier, StockReceipt, PriceHistory } from '@/types/inventory'
|
||||
import type { PaginatedResponse, PaginationInput } from '@lunarfront/shared/schemas'
|
||||
|
||||
// ─── Categories ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const categoryKeys = {
|
||||
all: ['categories'] as const,
|
||||
list: (params: PaginationInput) => [...categoryKeys.all, 'list', params] as const,
|
||||
allCategories: [...['categories'], 'all-flat'] as const,
|
||||
detail: (id: string) => [...categoryKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function categoryListOptions(params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: categoryKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<Category>>('/v1/categories', params as Record<string, unknown>),
|
||||
})
|
||||
}
|
||||
|
||||
export function categoryAllOptions() {
|
||||
return queryOptions({
|
||||
queryKey: categoryKeys.allCategories,
|
||||
queryFn: () => api.get<{ data: Category[] }>('/v1/categories/all'),
|
||||
})
|
||||
}
|
||||
|
||||
export const categoryMutations = {
|
||||
create: (data: Record<string, unknown>) => api.post<Category>('/v1/categories', data),
|
||||
update: (id: string, data: Record<string, unknown>) => api.patch<Category>(`/v1/categories/${id}`, data),
|
||||
delete: (id: string) => api.del<Category>(`/v1/categories/${id}`),
|
||||
}
|
||||
|
||||
// ─── Suppliers ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const supplierKeys = {
|
||||
all: ['suppliers'] as const,
|
||||
list: (params: PaginationInput) => [...supplierKeys.all, 'list', params] as const,
|
||||
detail: (id: string) => [...supplierKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function supplierListOptions(params: PaginationInput) {
|
||||
return queryOptions({
|
||||
queryKey: supplierKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<Supplier>>('/v1/suppliers', params as Record<string, unknown>),
|
||||
})
|
||||
}
|
||||
|
||||
export const supplierMutations = {
|
||||
create: (data: Record<string, unknown>) => api.post<Supplier>('/v1/suppliers', data),
|
||||
update: (id: string, data: Record<string, unknown>) => api.patch<Supplier>(`/v1/suppliers/${id}`, data),
|
||||
delete: (id: string) => api.del<Supplier>(`/v1/suppliers/${id}`),
|
||||
}
|
||||
|
||||
// ─── Products ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const productKeys = {
|
||||
all: ['products'] as const,
|
||||
list: (params: Record<string, unknown>) => [...productKeys.all, 'list', params] as const,
|
||||
detail: (id: string) => [...productKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function productListOptions(params: Record<string, unknown>) {
|
||||
return queryOptions({
|
||||
queryKey: productKeys.list(params),
|
||||
queryFn: () => api.get<PaginatedResponse<Product>>('/v1/products', params),
|
||||
})
|
||||
}
|
||||
|
||||
export function productDetailOptions(id: string) {
|
||||
return queryOptions({
|
||||
queryKey: productKeys.detail(id),
|
||||
queryFn: () => api.get<Product>(`/v1/products/${id}`),
|
||||
enabled: !!id,
|
||||
})
|
||||
}
|
||||
|
||||
export const productMutations = {
|
||||
create: (data: Record<string, unknown>) => api.post<Product>('/v1/products', data),
|
||||
update: (id: string, data: Record<string, unknown>) => api.patch<Product>(`/v1/products/${id}`, data),
|
||||
delete: (id: string) => api.del<Product>(`/v1/products/${id}`),
|
||||
}
|
||||
|
||||
// ─── Inventory Units ─────────────────────────────────────────────────────────
|
||||
|
||||
export const unitKeys = {
|
||||
all: ['inventory-units'] as const,
|
||||
byProduct: (productId: string) => [...unitKeys.all, 'product', productId] as const,
|
||||
detail: (id: string) => [...unitKeys.all, 'detail', id] as const,
|
||||
}
|
||||
|
||||
export function unitListOptions(productId: string) {
|
||||
return queryOptions({
|
||||
queryKey: unitKeys.byProduct(productId),
|
||||
queryFn: () => api.get<{ data: InventoryUnit[] }>(`/v1/products/${productId}/units`),
|
||||
enabled: !!productId,
|
||||
})
|
||||
}
|
||||
|
||||
export const unitMutations = {
|
||||
create: (productId: string, data: Record<string, unknown>) =>
|
||||
api.post<InventoryUnit>(`/v1/products/${productId}/units`, data),
|
||||
update: (id: string, data: Record<string, unknown>) =>
|
||||
api.patch<InventoryUnit>(`/v1/units/${id}`, data),
|
||||
}
|
||||
|
||||
// ─── Product Suppliers ───────────────────────────────────────────────────────
|
||||
|
||||
export const productSupplierKeys = {
|
||||
byProduct: (productId: string) => ['products', productId, 'suppliers'] as const,
|
||||
}
|
||||
|
||||
export function productSupplierListOptions(productId: string) {
|
||||
return queryOptions({
|
||||
queryKey: productSupplierKeys.byProduct(productId),
|
||||
queryFn: () => api.get<{ data: ProductSupplier[] }>(`/v1/products/${productId}/suppliers`),
|
||||
enabled: !!productId,
|
||||
})
|
||||
}
|
||||
|
||||
export const productSupplierMutations = {
|
||||
create: (productId: string, data: Record<string, unknown>) =>
|
||||
api.post<ProductSupplier>(`/v1/products/${productId}/suppliers`, data),
|
||||
update: (productId: string, id: string, data: Record<string, unknown>) =>
|
||||
api.patch<ProductSupplier>(`/v1/products/${productId}/suppliers/${id}`, data),
|
||||
delete: (productId: string, id: string) =>
|
||||
api.del(`/v1/products/${productId}/suppliers/${id}`),
|
||||
}
|
||||
|
||||
// ─── Stock Receipts ──────────────────────────────────────────────────────────
|
||||
|
||||
export const stockReceiptKeys = {
|
||||
byProduct: (productId: string) => ['products', productId, 'stock-receipts'] as const,
|
||||
}
|
||||
|
||||
export function stockReceiptListOptions(productId: string) {
|
||||
return queryOptions({
|
||||
queryKey: stockReceiptKeys.byProduct(productId),
|
||||
queryFn: () => api.get<{ data: StockReceipt[] }>(`/v1/products/${productId}/stock-receipts`),
|
||||
enabled: !!productId,
|
||||
})
|
||||
}
|
||||
|
||||
export const stockReceiptMutations = {
|
||||
create: (productId: string, data: Record<string, unknown>) =>
|
||||
api.post<StockReceipt>(`/v1/products/${productId}/stock-receipts`, data),
|
||||
}
|
||||
|
||||
// ─── Price History ───────────────────────────────────────────────────────────
|
||||
|
||||
export const priceHistoryKeys = {
|
||||
byProduct: (productId: string) => ['products', productId, 'price-history'] as const,
|
||||
}
|
||||
|
||||
export function priceHistoryOptions(productId: string) {
|
||||
return queryOptions({
|
||||
queryKey: priceHistoryKeys.byProduct(productId),
|
||||
queryFn: () => api.get<{ data: PriceHistory[] }>(`/v1/products/${productId}/price-history`),
|
||||
enabled: !!productId,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user