124 lines
3.6 KiB
TypeScript
124 lines
3.6 KiB
TypeScript
import { Router, type Request, type Response } from 'express'
|
|
import { listFactories, listOrders } from '../db.js'
|
|
import { requireAuth } from '../middleware/requireAuth.js'
|
|
|
|
const router = Router()
|
|
|
|
router.use(requireAuth)
|
|
|
|
const cleanText = (v: unknown): string => {
|
|
if (v === null || v === undefined) return ''
|
|
return String(v).replace(/\u00A0/g, ' ').replace(/[\s\u3000]+/g, ' ').trim()
|
|
}
|
|
|
|
const isDone = (status: string): boolean => {
|
|
const s = cleanText(status)
|
|
if (!s) return false
|
|
return /完成|已出完|出完|结束|已完成/.test(s)
|
|
}
|
|
|
|
const isCanceled = (status: string): boolean => {
|
|
const s = cleanText(status)
|
|
if (!s) return false
|
|
return /取消|作废/.test(s)
|
|
}
|
|
|
|
const normalizeStage = (status: string): string => {
|
|
const s = cleanText(status)
|
|
if (!s) return '未填写'
|
|
if (isCanceled(s)) return '取消'
|
|
if (isDone(s)) return '完成'
|
|
if (/验货|驗貨|待验货|已验货/.test(s)) return '验货'
|
|
if (/订舱|訂艙|订仓|订仓/.test(s)) return '订舱'
|
|
if (/包材|唛头|嘜頭/.test(s)) return '包材唛头'
|
|
if (/不干胶|外箱/.test(s)) return '外箱不干胶'
|
|
if (/出货|出運|出运|发货|發貨|已出货|待送货/.test(s)) return '出货'
|
|
if (/生产|待生产|排版|催大货样/.test(s)) return '生产'
|
|
if (/下单|下單/.test(s)) return '下单'
|
|
return '其他'
|
|
}
|
|
|
|
const dateKey = (s?: string): string => {
|
|
const v = cleanText(s)
|
|
return v ? v.slice(0, 10) : ''
|
|
}
|
|
|
|
const lastNDaysKeys = (n: number): string[] => {
|
|
const out: string[] = []
|
|
const now = new Date()
|
|
now.setHours(0, 0, 0, 0)
|
|
for (let i = n - 1; i >= 0; i -= 1) {
|
|
const d = new Date(now)
|
|
d.setDate(d.getDate() - i)
|
|
out.push(d.toISOString().slice(0, 10))
|
|
}
|
|
return out
|
|
}
|
|
|
|
router.get('/dashboard', async (_req: Request, res: Response): Promise<void> => {
|
|
const orders = await listOrders()
|
|
const factories = await listFactories()
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
|
|
const active = orders.filter((o) => !isDone(o.orderProgress) && !isCanceled(o.orderProgress))
|
|
const totalAmount = orders.reduce((sum, o) => sum + (o.orderAmount ?? 0), 0)
|
|
const overdue = active.filter((o) => {
|
|
const due = dateKey(o.customerDeliveryDate)
|
|
if (!due) return false
|
|
return due < today
|
|
})
|
|
|
|
const statusMap = new Map<string, number>()
|
|
for (const o of orders) {
|
|
const stage = normalizeStage(o.orderProgress)
|
|
statusMap.set(stage, (statusMap.get(stage) ?? 0) + 1)
|
|
}
|
|
const statusDist = Array.from(statusMap.entries()).map(([name, value]) => ({
|
|
name,
|
|
value,
|
|
}))
|
|
|
|
const factoryNameById = new Map(factories.map((f) => [f.id, f.name]))
|
|
const factoryMap = new Map<string, number>()
|
|
for (const o of orders) {
|
|
const key = (o.factoryId ? factoryNameById.get(o.factoryId) : undefined) ?? '未指定'
|
|
factoryMap.set(key, (factoryMap.get(key) ?? 0) + 1)
|
|
}
|
|
const factoryDist = Array.from(factoryMap.entries()).map(([name, value]) => ({
|
|
name,
|
|
value,
|
|
}))
|
|
|
|
const days = lastNDaysKeys(30)
|
|
const seriesMap = new Map(days.map((d) => [d, { date: d, amount: 0, count: 0 }]))
|
|
for (const o of orders) {
|
|
const d = dateKey(o.orderDate) || dateKey(o.createdAt)
|
|
if (!d || !seriesMap.has(d)) continue
|
|
const row = seriesMap.get(d)!
|
|
row.amount += o.orderAmount ?? 0
|
|
row.count += 1
|
|
}
|
|
|
|
const series = days.map((d) => seriesMap.get(d)!)
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
kpis: {
|
|
totalOrders: orders.length,
|
|
activeOrders: active.length,
|
|
overdueOrders: overdue.length,
|
|
totalAmount,
|
|
},
|
|
charts: {
|
|
statusDist,
|
|
factoryDist,
|
|
trend: series,
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
export default router
|
|
|