JessieExcel/api/routes/orders.ts
2026-03-25 01:54:12 +08:00

464 lines
14 KiB
TypeScript

import { Router, type Request, type Response } from 'express'
import { z } from 'zod'
import {
addProgressEvent,
bulkCreateOrders,
bulkDeleteOrders,
clearOrders,
createOrder,
deleteOrder,
getOrder,
listFactories,
listOrders,
listProgressEvents,
updateOrder,
} from '../db.js'
import type { AuthedRequest } from '../middleware/requireAuth.js'
import { requireAuth } from '../middleware/requireAuth.js'
import type { Order } from '../../shared/types.js'
const router = Router()
const parseNumber = (v: unknown): number | undefined => {
if (typeof v === 'number' && Number.isFinite(v)) return v
if (typeof v === 'string' && v.trim().length > 0 && Number.isFinite(Number(v))) {
return Number(v)
}
return undefined
}
const dateOrUndef = (v: unknown): string | undefined => {
if (typeof v !== 'string') return undefined
const s = v.trim()
return s.length > 0 ? s : undefined
}
const createOrderSchema = z.object({
orderNo: z.string().min(1),
poNo: z.string().optional(),
productName: z.string().min(1),
country: z.string().optional(),
orderAmount: z.union([z.number(), z.string()]).optional(),
orderDate: z.string().optional(),
customerDeliveryDate: z.string().optional(),
factoryId: z.string().optional(),
factoryDeliveryDate: z.string().optional(),
factoryContract: z.string().optional(),
packagingStatus: z.string().optional(),
stickerStatus: z.string().optional(),
shippingStatus: z.string().optional(),
inspectionStatus: z.string().optional(),
purchaseAmount: z.union([z.number(), z.string()]).optional(),
orderProgress: z.string().optional(),
remarks: z.string().optional(),
ciAmount: z.union([z.number(), z.string()]).optional(),
etd: z.string().optional(),
eta: z.string().optional(),
paymentDate: z.string().optional(),
paymentAmount: z.union([z.number(), z.string()]).optional(),
balance: z.union([z.number(), z.string()]).optional(),
})
const updateOrderSchema = createOrderSchema.partial()
const duplicatesSchema = z.object({
orderNos: z.array(z.string().min(1)).max(10000),
})
const bulkSchema = z.object({
items: z.array(createOrderSchema).min(1).max(5000),
skipDuplicates: z.boolean().optional(),
})
const deleteManySchema = z.object({
ids: z.array(z.string().min(1)).min(1).max(10000),
})
router.use(requireAuth)
router.get('/', async (req: Request, res: Response): Promise<void> => {
const q = req.query
const page = Math.max(1, Number(q.page ?? 1) || 1)
const pageSize = Math.min(100, Math.max(1, Number(q.pageSize ?? 20) || 20))
const orderNo = typeof q.orderNo === 'string' ? q.orderNo.trim() : ''
const poNo = typeof q.poNo === 'string' ? q.poNo.trim() : ''
const productName = typeof q.productName === 'string' ? q.productName.trim() : ''
const country = typeof q.country === 'string' ? q.country.trim() : ''
const factoryId = typeof q.factoryId === 'string' ? q.factoryId.trim() : ''
const progress = typeof q.orderProgress === 'string' ? q.orderProgress.trim() : ''
const overdue = q.overdue === 'true'
const dateFrom = typeof q.dateFrom === 'string' ? q.dateFrom.trim() : ''
const dateTo = typeof q.dateTo === 'string' ? q.dateTo.trim() : ''
const orders = await listOrders()
const now = new Date().toISOString().slice(0, 10)
const filtered = orders.filter((o) => {
if (orderNo && !o.orderNo.toLowerCase().includes(orderNo.toLowerCase())) return false
if (poNo && !(o.poNo ?? '').toLowerCase().includes(poNo.toLowerCase())) return false
if (
productName &&
!o.productName.toLowerCase().includes(productName.toLowerCase())
) {
return false
}
if (country && !(o.country ?? '').toLowerCase().includes(country.toLowerCase())) {
return false
}
if (factoryId && (o.factoryId ?? '') !== factoryId) return false
if (progress && o.orderProgress !== progress) return false
if (dateFrom && (o.orderDate ?? '') < dateFrom) return false
if (dateTo && (o.orderDate ?? '') > dateTo) return false
if (overdue) {
const due = o.customerDeliveryDate
if (!due) return false
if (o.orderProgress === '完成' || o.orderProgress === '取消') return false
if (due >= now) return false
}
return true
})
const total = filtered.length
const start = (page - 1) * pageSize
const items = filtered.slice(start, start + pageSize)
res.json({ success: true, data: { items, total, page, pageSize } })
})
router.post('/duplicates', async (req: Request, res: Response): Promise<void> => {
const parsed = duplicatesSchema.safeParse(req.body)
if (!parsed.success) {
res.status(400).json({ success: false, error: 'BAD_REQUEST' })
return
}
const orders = await listOrders()
const existing = new Set(orders.map((o) => o.orderNo))
const duplicates = parsed.data.orderNos.filter((x) => existing.has(x))
res.json({ success: true, data: { duplicates } })
})
router.post('/bulk', async (req: Request, res: Response): Promise<void> => {
const user = (req as AuthedRequest).user
if (!['sales', 'admin'].includes(user.role)) {
res.status(403).json({ success: false, error: 'FORBIDDEN' })
return
}
const parsed = bulkSchema.safeParse(req.body)
if (!parsed.success) {
res.status(400).json({ success: false, error: 'BAD_REQUEST' })
return
}
const items = parsed.data.items
const inputs: Omit<Order, 'id' | 'createdAt' | 'updatedAt'>[] = items.map((body) => {
const orderProgress = (body.orderProgress ?? '下单').trim() || '下单'
return {
orderNo: body.orderNo,
poNo: body.poNo,
productName: body.productName,
country: body.country,
orderAmount: parseNumber(body.orderAmount),
orderDate: dateOrUndef(body.orderDate),
customerDeliveryDate: dateOrUndef(body.customerDeliveryDate),
factoryId: body.factoryId,
factoryDeliveryDate: dateOrUndef(body.factoryDeliveryDate),
factoryContract: body.factoryContract,
packagingStatus: body.packagingStatus,
stickerStatus: body.stickerStatus,
shippingStatus: body.shippingStatus,
inspectionStatus: body.inspectionStatus,
purchaseAmount: parseNumber(body.purchaseAmount),
orderProgress,
remarks: body.remarks,
ciAmount: parseNumber(body.ciAmount),
etd: dateOrUndef(body.etd),
eta: dateOrUndef(body.eta),
paymentDate: dateOrUndef(body.paymentDate),
paymentAmount: parseNumber(body.paymentAmount),
balance: parseNumber(body.balance),
createdByUserId: user.id,
}
})
const result = await bulkCreateOrders({
inputs,
createdByUserId: user.id,
skipDuplicates: parsed.data.skipDuplicates ?? true,
})
res.json({
success: true,
data: {
createdCount: result.created.length,
skippedOrderNos: result.skippedOrderNos,
},
})
})
router.post('/deleteMany', async (req: Request, res: Response): Promise<void> => {
const user = (req as AuthedRequest).user
if (!['sales', 'admin'].includes(user.role)) {
res.status(403).json({ success: false, error: 'FORBIDDEN' })
return
}
const parsed = deleteManySchema.safeParse(req.body)
if (!parsed.success) {
res.status(400).json({ success: false, error: 'BAD_REQUEST' })
return
}
const removed = await bulkDeleteOrders({ ids: parsed.data.ids })
res.json({ success: true, data: { removed } })
})
router.post('/clear', async (req: Request, res: Response): Promise<void> => {
const user = (req as AuthedRequest).user
if (!['sales', 'admin'].includes(user.role)) {
res.status(403).json({ success: false, error: 'FORBIDDEN' })
return
}
const removed = await clearOrders()
res.json({ success: true, data: { removed } })
})
router.get('/:id', async (req: Request, res: Response): Promise<void> => {
const id = req.params.id
const order = await getOrder(id)
if (!order) {
res.status(404).json({ success: false, error: 'NOT_FOUND' })
return
}
const factories = await listFactories()
const factory = factories.find((f) => f.id === order.factoryId)
const progressEvents = await listProgressEvents(id)
res.json({ success: true, data: { order, factory, progressEvents } })
})
router.post('/', async (req: Request, res: Response): Promise<void> => {
const user = (req as AuthedRequest).user
if (!['sales', 'admin'].includes(user.role)) {
res.status(403).json({ success: false, error: 'FORBIDDEN' })
return
}
const parsed = createOrderSchema.safeParse(req.body)
if (!parsed.success) {
res.status(400).json({ success: false, error: 'BAD_REQUEST' })
return
}
const body = parsed.data
const orderProgress = (body.orderProgress ?? '下单').trim() || '下单'
const input: Omit<Order, 'id' | 'createdAt' | 'updatedAt'> = {
orderNo: body.orderNo,
poNo: body.poNo,
productName: body.productName,
country: body.country,
orderAmount: parseNumber(body.orderAmount),
orderDate: dateOrUndef(body.orderDate),
customerDeliveryDate: dateOrUndef(body.customerDeliveryDate),
factoryId: body.factoryId,
factoryDeliveryDate: dateOrUndef(body.factoryDeliveryDate),
factoryContract: body.factoryContract,
packagingStatus: body.packagingStatus,
stickerStatus: body.stickerStatus,
shippingStatus: body.shippingStatus,
inspectionStatus: body.inspectionStatus,
purchaseAmount: parseNumber(body.purchaseAmount),
orderProgress,
remarks: body.remarks,
ciAmount: parseNumber(body.ciAmount),
etd: dateOrUndef(body.etd),
eta: dateOrUndef(body.eta),
paymentDate: dateOrUndef(body.paymentDate),
paymentAmount: parseNumber(body.paymentAmount),
balance: parseNumber(body.balance),
createdByUserId: user.id,
}
const created = await createOrder({ input })
await addProgressEvent({
orderId: created.id,
status: created.orderProgress,
note: '创建订单',
createdByUserId: user.id,
})
res.json({ success: true, data: created })
})
router.put('/:id', async (req: Request, res: Response): Promise<void> => {
const user = (req as AuthedRequest).user
const id = req.params.id
const existed = await getOrder(id)
if (!existed) {
res.status(404).json({ success: false, error: 'NOT_FOUND' })
return
}
const parsed = updateOrderSchema.safeParse(req.body)
if (!parsed.success) {
res.status(400).json({ success: false, error: 'BAD_REQUEST' })
return
}
const body = parsed.data
const isAdmin = user.role === 'admin'
const isSales = user.role === 'sales'
const isPurchase = user.role === 'purchase'
if (!isAdmin && !isSales && !isPurchase) {
res.status(403).json({ success: false, error: 'FORBIDDEN' })
return
}
const allowedFields: (keyof Order)[] = isAdmin
? [
'orderNo',
'poNo',
'productName',
'country',
'orderAmount',
'orderDate',
'customerDeliveryDate',
'factoryId',
'factoryDeliveryDate',
'factoryContract',
'packagingStatus',
'stickerStatus',
'shippingStatus',
'inspectionStatus',
'purchaseAmount',
'orderProgress',
'remarks',
'ciAmount',
'etd',
'eta',
'paymentDate',
'paymentAmount',
'balance',
]
: isSales
? [
'orderNo',
'poNo',
'productName',
'country',
'orderAmount',
'orderDate',
'customerDeliveryDate',
'factoryId',
'remarks',
'ciAmount',
'paymentDate',
'paymentAmount',
'balance',
]
: [
'factoryDeliveryDate',
'factoryContract',
'packagingStatus',
'stickerStatus',
'shippingStatus',
'inspectionStatus',
'purchaseAmount',
'orderProgress',
'etd',
'eta',
]
const patch: Partial<Order> = {}
for (const key of allowedFields) {
if (!(key in body)) continue
const value = (body as Record<string, unknown>)[key]
if (key.endsWith('Amount') || key === 'balance') {
(patch as Record<string, unknown>)[key] = parseNumber(value)
continue
}
if (
key.endsWith('Date') ||
key === 'etd' ||
key === 'eta'
) {
(patch as Record<string, unknown>)[key] = dateOrUndef(value)
continue
}
(patch as Record<string, unknown>)[key] = value
}
const updated = await updateOrder({ id, patch })
if (!updated) {
res.status(404).json({ success: false, error: 'NOT_FOUND' })
return
}
if (patch.orderProgress && patch.orderProgress !== existed.orderProgress) {
await addProgressEvent({
orderId: updated.id,
status: patch.orderProgress,
note: '更新订单状态',
createdByUserId: user.id,
})
}
res.json({ success: true, data: updated })
})
router.delete('/:id', async (req: Request, res: Response): Promise<void> => {
const user = (req as AuthedRequest).user
if (!['sales', 'admin'].includes(user.role)) {
res.status(403).json({ success: false, error: 'FORBIDDEN' })
return
}
const ok = await deleteOrder(req.params.id)
if (!ok) {
res.status(404).json({ success: false, error: 'NOT_FOUND' })
return
}
res.json({ success: true })
})
const progressSchema = z.object({
status: z.string().min(1),
note: z.string().optional(),
})
router.post('/:id/progress', async (req: Request, res: Response): Promise<void> => {
const user = (req as AuthedRequest).user
if (!['sales', 'purchase', 'admin'].includes(user.role)) {
res.status(403).json({ success: false, error: 'FORBIDDEN' })
return
}
const orderId = req.params.id
const existed = await getOrder(orderId)
if (!existed) {
res.status(404).json({ success: false, error: 'NOT_FOUND' })
return
}
const parsed = progressSchema.safeParse(req.body)
if (!parsed.success) {
res.status(400).json({ success: false, error: 'BAD_REQUEST' })
return
}
const evt = await addProgressEvent({
orderId,
status: parsed.data.status,
note: parsed.data.note,
createdByUserId: user.id,
})
if (parsed.data.status !== existed.orderProgress) {
await updateOrder({ id: orderId, patch: { orderProgress: parsed.data.status } })
}
res.json({ success: true, data: evt })
})
export default router