风险与内控管理优化: 异常账单服务端分页/筛选/收银员搜索/合计, 收银员TOP15按异常账单数排序, 双击跳转明细, MonthPicker靠右, 侧边栏底部间距

This commit is contained in:
freedakgmail
2026-08-01 21:28:15 +08:00
parent ac8e39e2f9
commit d724a565f5
60 changed files with 3154 additions and 2561 deletions
+103 -55
View File
@@ -10,6 +10,7 @@ const router = Router()
// 成本总览指标
router.get('/overview', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
SELECT
count(*) AS total_dishes,
@@ -22,7 +23,8 @@ router.get('/overview', async (req: AuthRequest, res) => {
round(sum(actual_cost)::numeric, 2) AS total_actual_cost,
round(sum(cost_variance_amount)::numeric, 2) AS total_variance
FROM public.dish_cost_analysis_summary
`)
WHERE import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
`, [month])
sendSuccess(res, result.rows[0])
} catch (err: any) {
sendError(res, err.message)
@@ -32,6 +34,7 @@ router.get('/overview', async (req: AuthRequest, res) => {
// 品类成本对比
router.get('/category-comparison', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
SELECT category_level1,
count(*) AS dishes,
@@ -43,8 +46,9 @@ router.get('/category-comparison', async (req: AuthRequest, res) => {
round(sum(cost_variance_amount)::numeric, 2) AS total_variance
FROM public.dish_cost_analysis_summary
WHERE category_level1 IS NOT NULL
AND import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
GROUP BY category_level1 ORDER BY total_sales DESC
`)
`, [month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -54,6 +58,7 @@ router.get('/category-comparison', async (req: AuthRequest, res) => {
// 毛利率偏差分布
router.get('/margin-deviation', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
SELECT
CASE
@@ -66,8 +71,9 @@ router.get('/margin-deviation', async (req: AuthRequest, res) => {
count(*) AS cnt
FROM public.dish_cost_analysis_summary
WHERE theoretical_margin_rate_pct IS NOT NULL AND actual_margin_rate_pct IS NOT NULL
AND import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
GROUP BY deviation_band ORDER BY cnt DESC
`)
`, [month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -79,6 +85,7 @@ router.get('/variance-top', async (req: AuthRequest, res) => {
try {
const limit = parseInt((req.query.limit as string) || '50')
const order = (req.query.order as string) === 'asc' ? 'ASC' : 'DESC'
const month = parseMonth(req)
const result = await query(`
SELECT dish_name, dish_code, category_level1,
round(theoretical_margin_rate_pct::numeric, 2) AS theo_margin,
@@ -93,8 +100,9 @@ router.get('/variance-top', async (req: AuthRequest, res) => {
ELSE '正常'
END AS cost_tier
FROM public.dish_cost_analysis_summary
WHERE import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $2::date ORDER BY import_id DESC LIMIT 1)
ORDER BY cost_variance_amount ${order} LIMIT $1
`, [limit])
`, [limit, month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -106,24 +114,26 @@ router.get('/variance-top', async (req: AuthRequest, res) => {
// 菜单工程矩阵
router.get('/menu-engineering', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
WITH imp AS (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
SELECT dish_name, dish_code, category_level1,
round(sales_quantity::numeric, 2) AS sales_quantity,
round(sales_amount::numeric, 2) AS sales_amount,
round(actual_margin_rate_pct::numeric, 2) AS actual_margin,
CASE
WHEN actual_margin_rate_pct < 0 THEN '数据异常'
WHEN sales_quantity >= (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY sales_quantity) FROM public.dish_cost_analysis_summary)
AND actual_margin_rate_pct >= (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY actual_margin_rate_pct) FROM public.dish_cost_analysis_summary WHERE actual_margin_rate_pct > 0) THEN '明星盈利品'
WHEN sales_quantity >= (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY sales_quantity) FROM public.dish_cost_analysis_summary)
AND actual_margin_rate_pct < (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY actual_margin_rate_pct) FROM public.dish_cost_analysis_summary WHERE actual_margin_rate_pct > 0) THEN '高销低利品'
WHEN sales_quantity < (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY sales_quantity) FROM public.dish_cost_analysis_summary)
AND actual_margin_rate_pct >= (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY actual_margin_rate_pct) FROM public.dish_cost_analysis_summary WHERE actual_margin_rate_pct > 0) THEN '低销高利品'
WHEN sales_quantity >= (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY sales_quantity) FROM public.dish_cost_analysis_summary s, imp WHERE s.import_id = imp.import_id)
AND actual_margin_rate_pct >= (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY actual_margin_rate_pct) FROM public.dish_cost_analysis_summary s, imp WHERE s.import_id = imp.import_id AND actual_margin_rate_pct > 0) THEN '明星盈利品'
WHEN sales_quantity >= (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY sales_quantity) FROM public.dish_cost_analysis_summary s, imp WHERE s.import_id = imp.import_id)
AND actual_margin_rate_pct < (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY actual_margin_rate_pct) FROM public.dish_cost_analysis_summary s, imp WHERE s.import_id = imp.import_id AND actual_margin_rate_pct > 0) THEN '高销低利品'
WHEN sales_quantity < (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY sales_quantity) FROM public.dish_cost_analysis_summary s, imp WHERE s.import_id = imp.import_id)
AND actual_margin_rate_pct >= (SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY actual_margin_rate_pct) FROM public.dish_cost_analysis_summary s, imp WHERE s.import_id = imp.import_id AND actual_margin_rate_pct > 0) THEN '低销高利品'
ELSE '低销低利品'
END AS menu_type
FROM public.dish_cost_analysis_summary
WHERE actual_margin_rate_pct IS NOT NULL
`)
FROM public.dish_cost_analysis_summary s, imp
WHERE s.import_id = imp.import_id AND actual_margin_rate_pct IS NOT NULL
`, [month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -141,8 +151,9 @@ router.get('/profitability', async (req: AuthRequest, res) => {
const order = (req.query.order as string) === 'asc' ? 'ASC' : 'DESC'
const filter = (req.query.filter as string) || ''
let where = 'WHERE actual_margin_rate_pct IS NOT NULL'
const params: any[] = []
const month = parseMonth(req)
let where = 'WHERE actual_margin_rate_pct IS NOT NULL AND import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)'
const params: any[] = [month]
if (category) { params.push(category); where += ` AND category_level1 = $${params.length}` }
if (filter === 'profitable') { where += ` AND actual_margin_rate_pct > 0` }
else if (filter === 'loss') { where += ` AND actual_margin_rate_pct <= 0` }
@@ -177,6 +188,7 @@ router.get('/profitability', async (req: AuthRequest, res) => {
router.get('/pricing', async (req: AuthRequest, res) => {
try {
const threshold = parseFloat((req.query.threshold as string) || '50')
const month = parseMonth(req)
const result = await query(`
SELECT dish_name, dish_code, category_level1,
round(price::numeric, 2) AS price,
@@ -187,8 +199,9 @@ router.get('/pricing', async (req: AuthRequest, res) => {
round(sales_amount::numeric, 2) AS sales_amount
FROM public.dish_cost_analysis_summary
WHERE theoretical_margin_rate_pct < $1 AND sales_amount > 1000
AND import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $2::date ORDER BY import_id DESC LIMIT 1)
ORDER BY theoretical_margin_rate_pct ASC
`, [threshold])
`, [threshold, month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -202,6 +215,7 @@ router.get('/material-variance', async (req: AuthRequest, res) => {
try {
const dishCode = req.query.dish_code as string
if (!dishCode) return sendError(res, 'dish_code is required')
const month = parseMonth(req)
const result = await query(`
SELECT m.material_name, m.material_unit,
@@ -221,8 +235,9 @@ router.get('/material-variance', async (req: AuthRequest, res) => {
FROM public.dish_cost_analysis_material_detail m
JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id
WHERE s.dish_code = $1
AND s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $2::date ORDER BY import_id DESC LIMIT 1)
ORDER BY m.loss_amount ASC
`, [dishCode])
`, [dishCode, month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -232,6 +247,7 @@ router.get('/material-variance', async (req: AuthRequest, res) => {
// 损耗率分布
router.get('/loss-distribution', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
SELECT
CASE
@@ -244,9 +260,11 @@ router.get('/loss-distribution', async (req: AuthRequest, res) => {
ELSE '正向(>0%)'
END AS loss_band,
count(*) AS cnt
FROM public.dish_cost_analysis_material_detail
FROM public.dish_cost_analysis_material_detail m
JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id
WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
GROUP BY loss_band ORDER BY cnt DESC
`)
`, [month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -258,6 +276,7 @@ router.get('/material-loss-top', async (req: AuthRequest, res) => {
try {
const limit = parseInt((req.query.limit as string) || '50')
const order = (req.query.order as string) === 'rate' ? 'avg_loss_rate' : 'total_loss_qty'
const month = parseMonth(req)
const result = await query(`
SELECT material_name, material_type,
count(DISTINCT m.summary_id) AS dish_count,
@@ -266,10 +285,12 @@ router.get('/material-loss-top', async (req: AuthRequest, res) => {
round(max(loss_quantity_rate_pct)::numeric, 2) AS max_loss_rate,
round(sum(loss_amount)::numeric, 2) AS total_loss_amount
FROM public.dish_cost_analysis_material_detail m
JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id
WHERE m.loss_quantity < 0
AND s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $2::date ORDER BY import_id DESC LIMIT 1)
GROUP BY material_name, material_type
ORDER BY ${order} ASC LIMIT $1
`, [limit])
`, [limit, month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -279,15 +300,18 @@ router.get('/material-loss-top', async (req: AuthRequest, res) => {
// 物料类型损耗对比
router.get('/material-type-loss', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
SELECT material_type,
count(*) AS cnt,
round((sum(loss_quantity) / nullif(sum(theoretical_quantity), 0) * 100)::numeric, 2) AS avg_loss_rate,
round(sum(loss_amount)::numeric, 2) AS total_loss_amount,
count(DISTINCT material_name) AS unique_materials
FROM public.dish_cost_analysis_material_detail
FROM public.dish_cost_analysis_material_detail m
JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id
WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
GROUP BY material_type ORDER BY total_loss_amount ASC
`)
`, [month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)
@@ -582,15 +606,18 @@ router.get('/material-demand', async (req: AuthRequest, res) => {
// 包装总览
router.get('/packaging-overview', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
SELECT
count(DISTINCT material_name) AS packaging_types,
round(sum(theoretical_amount)::numeric, 2) AS total_cost,
round(sum(loss_quantity)::numeric, 2) AS total_loss_qty,
count(*) FILTER (WHERE loss_quantity_rate_pct < -20) AS high_loss_count
FROM public.dish_cost_analysis_material_detail
FROM public.dish_cost_analysis_material_detail m
JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id
WHERE material_name ~* '餐盒|餐具|打包|纸巾|餐盒|调味包|餐盒|碗|袋|杯|盒'
`)
AND s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
`, [month])
sendSuccess(res, result.rows[0])
} catch (err: any) {
sendError(res, err.message)
@@ -612,30 +639,35 @@ router.get('/packaging-detail', async (req: AuthRequest, res) => {
const validSorts = ['total_cost', 'total_qty', 'loss_qty', 'avg_loss_rate', 'dish_count', 'material_name']
const sortCol = validSorts.includes(sort) ? sort : 'total_cost'
const month = parseMonth(req)
const countResult = await query(`
SELECT count(DISTINCT material_name) FROM public.dish_cost_analysis_material_detail
SELECT count(DISTINCT material_name) FROM public.dish_cost_analysis_material_detail m
JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id
WHERE material_name ~* '餐盒|餐具|打包|纸巾|碗|袋|杯|盒'
`)
AND s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
`, [month])
const total = countResult.rows[0].count
const result = await query(`
SELECT material_name,
count(DISTINCT summary_id) AS dish_count,
round(sum(theoretical_quantity)::numeric, 2) AS total_qty,
round(sum(theoretical_amount)::numeric, 2) AS total_cost,
round(sum(loss_quantity)::numeric, 2) AS loss_qty,
round((sum(loss_quantity) / nullif(sum(theoretical_quantity), 0) * 100)::numeric, 2) AS avg_loss_rate,
count(DISTINCT m.summary_id) AS dish_count,
round(sum(m.theoretical_quantity)::numeric, 2) AS total_qty,
round(sum(m.theoretical_amount)::numeric, 2) AS total_cost,
round(sum(m.loss_quantity)::numeric, 2) AS loss_qty,
round((sum(m.loss_quantity) / nullif(sum(m.theoretical_quantity), 0) * 100)::numeric, 2) AS avg_loss_rate,
CASE
WHEN sum(loss_quantity) / nullif(sum(theoretical_quantity), 0) * 100 < -20 THEN '核查'
WHEN sum(loss_quantity) / nullif(sum(theoretical_quantity), 0) * 100 < -5 THEN '关注'
WHEN sum(m.loss_quantity) / nullif(sum(m.theoretical_quantity), 0) * 100 < -20 THEN '核查'
WHEN sum(m.loss_quantity) / nullif(sum(m.theoretical_quantity), 0) * 100 < -5 THEN '关注'
ELSE '正常'
END AS status
FROM public.dish_cost_analysis_material_detail
FROM public.dish_cost_analysis_material_detail m
JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id
WHERE material_name ~* '餐盒|餐具|打包|纸巾|碗|袋|杯|盒'
AND s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
GROUP BY material_name
${having}
ORDER BY ${sortCol} ${order} NULLS LAST LIMIT $1 OFFSET $2
`, [pageSize, offset])
ORDER BY ${sortCol} ${order} NULLS LAST LIMIT $2 OFFSET $3
`, [month, pageSize, offset])
sendSuccess(res, result.rows, { page, pageSize, total })
} catch (err: any) {
sendError(res, err.message)
@@ -647,19 +679,20 @@ router.get('/packaging-detail', async (req: AuthRequest, res) => {
// 数据质量总览
router.get('/data-quality', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
SELECT
(SELECT count(*) FROM analytics.dim_sku) AS total_sku,
(SELECT count(DISTINCT sku_code) FROM analytics.fact_recipe_bom) AS sku_with_bom,
round((SELECT count(DISTINCT sku_code)::numeric FROM analytics.fact_recipe_bom) / nullif((SELECT count(*) FROM analytics.dim_sku), 0) * 100, 2) AS bom_coverage,
(SELECT count(*) FROM analytics.v_dish_cost_analysis_latest_summary WHERE matched_to_dish_sales_by_name = true) AS matched_dishes,
(SELECT count(*) FROM analytics.v_dish_cost_analysis_latest_summary) AS total_dishes,
(SELECT count(*) FROM public.dish_cost_analysis_summary s WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1) AND EXISTS (SELECT 1 FROM public.dish_sales_details d WHERE d.dish_name = s.dish_name)) AS matched_dishes,
(SELECT count(*) FROM public.dish_cost_analysis_summary WHERE import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)) AS total_dishes,
(SELECT count(*) FROM analytics.fact_recipe_bom WHERE standard_net_quantity = 0) AS zero_actual_bom,
(SELECT count(*) FROM public.dish_cost_analysis_summary WHERE actual_margin_rate_pct < 0) AS negative_margin_count,
(SELECT count(*) FROM public.dish_cost_analysis_summary WHERE theoretical_margin_rate_pct < 0) AS negative_theo_margin_count,
(SELECT count(*) FROM analytics.v_dish_cost_analysis_latest_material_detail WHERE matched_to_inventory_by_name = true) AS matched_materials,
(SELECT count(*) FROM analytics.v_dish_cost_analysis_latest_material_detail) AS total_materials
`)
(SELECT count(*) FROM public.dish_cost_analysis_summary s WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1) AND s.actual_margin_rate_pct < 0) AS negative_margin_count,
(SELECT count(*) FROM public.dish_cost_analysis_summary s WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1) AND s.theoretical_margin_rate_pct < 0) AS negative_theo_margin_count,
(SELECT count(*) FROM public.dish_cost_analysis_material_detail m JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1) AND EXISTS (SELECT 1 FROM public.inventory_cost_records i WHERE i.material_name = m.material_name)) AS matched_materials,
(SELECT count(*) FROM public.dish_cost_analysis_material_detail m JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)) AS total_materials
`, [month])
sendSuccess(res, result.rows[0])
} catch (err: any) {
sendError(res, err.message)
@@ -676,16 +709,17 @@ router.get('/unmatched-dishes', async (req: AuthRequest, res) => {
const validSorts = ['sales_amount', 'dish_name', 'dish_code', 'category_level1']
const sortCol = validSorts.includes(sort) ? sort : 'sales_amount'
const countResult = await query(`SELECT count(*) FROM analytics.v_dish_cost_analysis_latest_summary WHERE matched_to_dish_sales_by_name = false`)
const month = parseMonth(req)
const countResult = await query(`SELECT count(*) FROM public.dish_cost_analysis_summary s WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1) AND NOT EXISTS (SELECT 1 FROM public.dish_sales_details d WHERE d.dish_name = s.dish_name)`, [month])
const total = countResult.rows[0].count
const result = await query(`
SELECT dish_name, dish_code, category_level1,
round(sales_amount::numeric, 2) AS sales_amount
FROM analytics.v_dish_cost_analysis_latest_summary
WHERE matched_to_dish_sales_by_name = false
ORDER BY ${sortCol} ${order} NULLS LAST LIMIT $1 OFFSET $2
`, [pageSize, offset])
FROM public.dish_cost_analysis_summary s
WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1) AND NOT EXISTS (SELECT 1 FROM public.dish_sales_details d WHERE d.dish_name = s.dish_name)
ORDER BY ${sortCol} ${order} NULLS LAST LIMIT $2 OFFSET $3
`, [month, pageSize, offset])
sendSuccess(res, result.rows, { page, pageSize, total })
} catch (err: any) {
sendError(res, err.message)
@@ -702,18 +736,30 @@ router.get('/unmatched-materials', async (req: AuthRequest, res) => {
const validSorts = ['dish_count', 'loss_amount', 'material_name', 'material_type']
const sortCol = validSorts.includes(sort) ? sort : 'dish_count'
const countResult = await query(`SELECT count(*) FROM analytics.v_dish_cost_analysis_latest_material_detail WHERE matched_to_inventory_by_name = false`)
const month = parseMonth(req)
const countResult = await query(`
SELECT count(*) FROM (
SELECT material_name, material_type
FROM public.dish_cost_analysis_material_detail m
JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id
WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
AND NOT EXISTS (SELECT 1 FROM public.inventory_cost_records i WHERE i.material_name = m.material_name)
GROUP BY material_name, material_type
) t
`, [month])
const total = countResult.rows[0].count
const result = await query(`
SELECT material_name, material_type,
count(DISTINCT summary_id) AS dish_count,
count(DISTINCT m.summary_id) AS dish_count,
round(sum(loss_amount)::numeric, 2) AS loss_amount
FROM analytics.v_dish_cost_analysis_latest_material_detail
WHERE matched_to_inventory_by_name = false
FROM public.dish_cost_analysis_material_detail m
JOIN public.dish_cost_analysis_summary s ON s.summary_id = m.summary_id
WHERE s.import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
AND NOT EXISTS (SELECT 1 FROM public.inventory_cost_records i WHERE i.material_name = m.material_name)
GROUP BY material_name, material_type
ORDER BY ${sortCol} ${order} NULLS LAST LIMIT $1 OFFSET $2
`, [pageSize, offset])
ORDER BY ${sortCol} ${order} NULLS LAST LIMIT $2 OFFSET $3
`, [month, pageSize, offset])
sendSuccess(res, result.rows, { page, pageSize, total })
} catch (err: any) {
sendError(res, err.message)
@@ -808,6 +854,7 @@ router.get('/store-ranking', async (req: AuthRequest, res) => {
// 散点图数据
router.get('/scatter', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
const result = await query(`
SELECT dish_name, dish_code, category_level1,
round(sales_quantity::numeric, 2) AS x,
@@ -815,7 +862,8 @@ router.get('/scatter', async (req: AuthRequest, res) => {
round(sales_amount::numeric, 2) AS size
FROM public.dish_cost_analysis_summary
WHERE cost_variance_amount IS NOT NULL AND sales_quantity IS NOT NULL
`)
AND import_id = (SELECT import_id FROM public.dish_cost_analysis_import_log WHERE report_month = $1::date ORDER BY import_id DESC LIMIT 1)
`, [month])
sendSuccess(res, result.rows)
} catch (err: any) {
sendError(res, err.message)