Phase 6-11: 驾驶舱图表/区域经理基准对比+周检录入/店长SKU备货+指标进度/门店详情7Tab/月度验收7Tab/区域汇总
- Phase 6: 驾驶舱新增象限散点图、P0/P1实收覆盖瀑布图、平台成本率TOP15柱状图 - Phase 7: 区域经理页面新增同类门店基准对比表、周检录入弹窗 - Phase 8: 店长页面新增核心SKU备货提醒、本周指标进度条,经营概览与进度并排 - Phase 9: 门店详情页7Tab(概览/餐段/品类/成本/会员/异常/任务),5个新API - Phase 10: 月度验收7Tab(验收汇总/升降级/完成率/活动清单/SKU治理/经验标准化/复盘优化),4个新API+阈值调整API - Phase 11: 前后端编译通过,API测试通过,浏览器验证0 error - 门店区域划分:从store_location_master填充dim_store.region(87家),v_region_summary视图+API+前端表格 - MetricCard增加text格式支持
This commit is contained in:
@@ -568,4 +568,107 @@ router.get('/ontology/metrics', async (req: AuthRequest, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// ============================================================
|
||||
// 月度复盘 API (Phase 10)
|
||||
// ============================================================
|
||||
|
||||
router.get('/monthly-review/completion', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
SELECT
|
||||
priority,
|
||||
count(*) as total,
|
||||
count(*) FILTER (WHERE status = '已验收' OR status = '已回滚') as completed,
|
||||
count(*) FILTER (WHERE verification_result = '达标') as passed,
|
||||
count(*) FILTER (WHERE verification_result = '改善中') as improving,
|
||||
count(*) FILTER (WHERE verification_result = '未改善') as failed,
|
||||
ROUND(count(*) FILTER (WHERE status = '已验收' OR status = '已回滚') * 100.0 / NULLIF(count(*), 0), 1) as completion_rate
|
||||
FROM analytics.store_task
|
||||
WHERE plan_month = $1
|
||||
GROUP BY priority
|
||||
ORDER BY priority
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
router.get('/monthly-review/activity-list', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const result = await query(`
|
||||
SELECT marketing_plan, bill_count, consumption, discounts, received,
|
||||
avg_bill_value, discount_rate_pct, theoretical_margin_pct,
|
||||
ROUND(discounts * 100.0 / NULLIF(consumption, 0), 2) as discount_cost_rate,
|
||||
ROUND(received - discounts, 2) as net_contribution
|
||||
FROM analytics.v_marketing_plan_summary
|
||||
ORDER BY received DESC
|
||||
`)
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
router.get('/monthly-review/sku-governance', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const abcResult = await query(`
|
||||
SELECT abc_class, count(*) as sku_count,
|
||||
ROUND(sum(revenue_share_pct), 1) as total_revenue_share,
|
||||
ROUND(avg(revenue_share_pct), 2) as avg_revenue_share
|
||||
FROM analytics.v_dish_sku_abc_april
|
||||
GROUP BY abc_class
|
||||
ORDER BY abc_class
|
||||
`)
|
||||
const longtailResult = await query(`
|
||||
SELECT dish_name, category_level1, bill_count, sales_quantity,
|
||||
received_amount, revenue_share_pct, cumulative_revenue_share
|
||||
FROM analytics.v_dish_sku_abc_april
|
||||
WHERE abc_class = 'C-长尾'
|
||||
ORDER BY received_amount ASC
|
||||
LIMIT 50
|
||||
`)
|
||||
sendSuccess(res, { summary: abcResult.rows, longtail: longtailResult.rows })
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
router.get('/monthly-review/indicator-effectiveness', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
SELECT t.problem_indicator,
|
||||
count(*) as task_count,
|
||||
count(*) FILTER (WHERE t.verification_result = '达标') as passed,
|
||||
count(*) FILTER (WHERE t.verification_result = '改善中') as improving,
|
||||
count(*) FILTER (WHERE t.verification_result = '未改善') as failed,
|
||||
ROUND(count(*) FILTER (WHERE t.verification_result = '达标') * 100.0 / NULLIF(count(*), 0), 1) as pass_rate,
|
||||
bool_and(t.current_value IS NOT NULL) as has_actual
|
||||
FROM analytics.store_task t
|
||||
WHERE t.plan_month = $1::date
|
||||
GROUP BY t.problem_indicator
|
||||
ORDER BY pass_rate DESC NULLS LAST
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows)
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
router.put('/indicators/:id/threshold', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id)
|
||||
const { yellow_threshold, red_threshold } = req.body
|
||||
const result = await query(`
|
||||
UPDATE analytics.indicator_dictionary
|
||||
SET yellow_threshold = $1, red_threshold = $2, version_date = CURRENT_DATE
|
||||
WHERE id = $3 RETURNING *
|
||||
`, [yellow_threshold, red_threshold, id])
|
||||
if (result.rows.length === 0) return sendError(res, 'Indicator not found', 404)
|
||||
await query(`
|
||||
INSERT INTO analytics.metric_version_log (indicator_id, indicator_name, change_type, old_value, new_value, change_reason)
|
||||
SELECT $1, indicator_name, 'threshold_adjust',
|
||||
concat('yellow=', COALESCE(yellow_threshold::text, 'NULL'), ', red=', COALESCE(red_threshold::text, 'NULL')),
|
||||
concat('yellow=', COALESCE($4::text, 'NULL'), ', red=', COALESCE($5::text, 'NULL')),
|
||||
'月度复盘阈值调整'
|
||||
FROM analytics.indicator_dictionary WHERE id = $1
|
||||
`, [id, yellow_threshold, red_threshold, yellow_threshold, red_threshold])
|
||||
sendSuccess(res, result.rows[0])
|
||||
} catch (err: any) { sendError(res, err.message) }
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
Reference in New Issue
Block a user