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:
@@ -26,6 +26,11 @@ export const inventoryRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
app.get('/categories/all', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const rows = await CategoryService.listAll(app.db)
|
||||
return reply.send({ data: rows })
|
||||
})
|
||||
|
||||
app.get('/categories/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const category = await CategoryService.getById(app.db, id)
|
||||
|
||||
@@ -4,9 +4,12 @@ import {
|
||||
ProductUpdateSchema,
|
||||
InventoryUnitCreateSchema,
|
||||
InventoryUnitUpdateSchema,
|
||||
ProductSupplierCreateSchema,
|
||||
ProductSupplierUpdateSchema,
|
||||
StockReceiptCreateSchema,
|
||||
PaginationSchema,
|
||||
} from '@lunarfront/shared/schemas'
|
||||
import { ProductService, InventoryUnitService } from '../../services/product.service.js'
|
||||
import { ProductService, InventoryUnitService, ProductSupplierService, StockReceiptService } from '../../services/product.service.js'
|
||||
|
||||
export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
// --- Products ---
|
||||
@@ -22,7 +25,16 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
|
||||
app.get('/products', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const params = PaginationSchema.parse(request.query)
|
||||
const result = await ProductService.list(app.db, params)
|
||||
const q = request.query as Record<string, string>
|
||||
const filters = {
|
||||
categoryId: q.categoryId,
|
||||
isActive: q.isActive === 'true' ? true : q.isActive === 'false' ? false : undefined,
|
||||
isSerialized: q.isSerialized === 'true' ? true : q.isSerialized === 'false' ? false : undefined,
|
||||
isRental: q.isRental === 'true' ? true : q.isRental === 'false' ? false : undefined,
|
||||
isDualUseRepair: q.isDualUseRepair === 'true' ? true : q.isDualUseRepair === 'false' ? false : undefined,
|
||||
lowStock: q.lowStock === 'true',
|
||||
}
|
||||
const result = await ProductService.list(app.db, params, filters)
|
||||
return reply.send(result)
|
||||
})
|
||||
|
||||
@@ -87,4 +99,66 @@ export const productRoutes: FastifyPluginAsync = async (app) => {
|
||||
if (!unit) return reply.status(404).send({ error: { message: 'Unit not found', statusCode: 404 } })
|
||||
return reply.send(unit)
|
||||
})
|
||||
|
||||
// --- Product Suppliers ---
|
||||
|
||||
app.get('/products/:productId/suppliers', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const { productId } = request.params as { productId: string }
|
||||
const rows = await ProductSupplierService.listByProduct(app.db, productId)
|
||||
return reply.send({ data: rows })
|
||||
})
|
||||
|
||||
app.post('/products/:productId/suppliers', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const { productId } = request.params as { productId: string }
|
||||
const parsed = ProductSupplierCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const row = await ProductSupplierService.create(app.db, productId, parsed.data)
|
||||
return reply.status(201).send(row)
|
||||
})
|
||||
|
||||
app.patch('/products/:productId/suppliers/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const { productId, id } = request.params as { productId: string; id: string }
|
||||
const parsed = ProductSupplierUpdateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const row = await ProductSupplierService.update(app.db, id, productId, parsed.data)
|
||||
if (!row) return reply.status(404).send({ error: { message: 'Not found', statusCode: 404 } })
|
||||
return reply.send(row)
|
||||
})
|
||||
|
||||
app.delete('/products/:productId/suppliers/:id', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const { id } = request.params as { id: string }
|
||||
const row = await ProductSupplierService.delete(app.db, id)
|
||||
if (!row) return reply.status(404).send({ error: { message: 'Not found', statusCode: 404 } })
|
||||
return reply.send(row)
|
||||
})
|
||||
|
||||
// --- Price History ---
|
||||
|
||||
app.get('/products/:productId/price-history', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const { productId } = request.params as { productId: string }
|
||||
const rows = await ProductService.listPriceHistory(app.db, productId)
|
||||
return reply.send({ data: rows })
|
||||
})
|
||||
|
||||
// --- Stock Receipts ---
|
||||
|
||||
app.post('/products/:productId/stock-receipts', { preHandler: [app.authenticate, app.requirePermission('inventory.edit')] }, async (request, reply) => {
|
||||
const { productId } = request.params as { productId: string }
|
||||
const parsed = StockReceiptCreateSchema.safeParse(request.body)
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
|
||||
}
|
||||
const receipt = await StockReceiptService.create(app.db, productId, parsed.data)
|
||||
return reply.status(201).send(receipt)
|
||||
})
|
||||
|
||||
app.get('/products/:productId/stock-receipts', { preHandler: [app.authenticate, app.requirePermission('inventory.view')] }, async (request, reply) => {
|
||||
const { productId } = request.params as { productId: string }
|
||||
const rows = await StockReceiptService.listByProduct(app.db, productId)
|
||||
return reply.send({ data: rows })
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user