风险与内控管理优化: 异常账单服务端分页/筛选/收银员搜索/合计, 收银员TOP15按异常账单数排序, 双击跳转明细, MonthPicker靠右, 侧边栏底部间距
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user