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 => { 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 => { 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 => { 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[] = 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 => { 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 => { 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 => { 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 => { 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 = { 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 => { 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 = {} for (const key of allowedFields) { if (!(key in body)) continue const value = (body as Record)[key] if (key.endsWith('Amount') || key === 'balance') { (patch as Record)[key] = parseNumber(value) continue } if ( key.endsWith('Date') || key === 'etd' || key === 'eta' ) { (patch as Record)[key] = dateOrUndef(value) continue } (patch as Record)[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 => { 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 => { 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