feat: 客流热力图增加在岗人员行+规则引擎面板+菜单名称调整
- 热力图每门店增加在岗人员行(前厅/后厨/管理/其他),从考勤打卡记录解析 - 新增traffic-heatmap-staffing API,解析考勤打卡时间计算每小时在岗人数 - 人员预测tab新增可折叠规则引擎面板,展示全部规则和触发条件 - 后端API返回ruleEngine字段(动态标准+优化规则+招聘规则+决策逻辑) - R9/R11/R12/R13增加占比超配约束,过滤不合理招聘建议 - 菜单项'菜品成本分析'→'菜品成本','门店费用分析'→'门店费用' - 热力图横向滚动条始终可见
This commit is contained in:
@@ -60,6 +60,92 @@ router.get('/meal-period-traffic', async (req: AuthRequest, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// 门店×小时在岗人员分布(从考勤打卡记录解析)
|
||||
router.get('/traffic-heatmap-staffing', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const storeName = req.query.store as string
|
||||
// 从考勤表取所有员工的打卡记录,解析上下班时间
|
||||
// department格式: 北京西部马华餐饮有限公司/西部马华品牌门店/.../七里庄店/前厅/管理组
|
||||
const result = await query(`
|
||||
SELECT department, position, day_15
|
||||
FROM attendance_records
|
||||
WHERE department LIKE '%西部马华品牌门店%'
|
||||
AND day_15 IS NOT NULL AND day_15 != ''
|
||||
`)
|
||||
|
||||
// 从department提取门店名(以"店"结尾的层级)
|
||||
function extractStore(dept: string): string {
|
||||
const parts = dept.split('/')
|
||||
for (let i = parts.length - 1; i >= 0; i--) {
|
||||
if (parts[i].endsWith('店')) return parts[i]
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 从打卡记录解析上下班时间,格式: 08:30(考勤机:指纹)+20:28(考勤机:指纹)
|
||||
function parseClockTimes(raw: string): { start: number; end: number } | null {
|
||||
const times = raw.match(/(\d{1,2}):(\d{2})/g)
|
||||
if (!times || times.length < 2) return null
|
||||
const startParts = times[0].match(/(\d{1,2}):(\d{2})/)
|
||||
const endParts = times[times.length - 1].match(/(\d{1,2}):(\d{2})/)
|
||||
if (!startParts || !endParts) return null
|
||||
const startHour = parseInt(startParts[1])
|
||||
let endHour = parseInt(endParts[1])
|
||||
// 如果下班时间小于上班时间,说明跨天,按23点算
|
||||
if (endHour < startHour) endHour = 23
|
||||
return { start: startHour, end: endHour }
|
||||
}
|
||||
|
||||
// 汇总:store -> hour -> { 前厅, 后厨, 管理, 其他 }
|
||||
const staffing: Record<string, Record<number, { 前厅: number; 后厨: number; 管理: number; 其他: number }>> = {}
|
||||
|
||||
for (const row of result.rows) {
|
||||
const store = extractStore(row.department as string)
|
||||
if (!store) continue
|
||||
if (storeName && store !== storeName) continue
|
||||
|
||||
const clock = parseClockTimes(row.day_15 as string)
|
||||
if (!clock) continue
|
||||
|
||||
// 用position做SQL LIKE风格的判断
|
||||
const pos = row.position as string
|
||||
const dept = row.department as string
|
||||
let role = '其他'
|
||||
if (pos.includes('店长') || pos.includes('经理') || pos.startsWith('储备') || pos.includes('副店')) role = '管理'
|
||||
else if (dept.includes('/前厅/') || pos.includes('服务员') || pos.includes('训练员') || pos.includes('迎宾') || pos.includes('传菜') || pos.includes('主管')) role = '前厅'
|
||||
else if (dept.includes('/后厨') || pos.includes('厨') || pos.includes('拉面') || pos.includes('配菜') || pos.includes('凉菜') || pos.includes('烧烤') || pos.includes('面点') || pos.includes('面工') || pos.includes('锅底') || pos.includes('切肉') || pos.includes('切菜') || pos.includes('炒锅') || pos.includes('砧板') || pos.includes('打荷') || pos.includes('洗碗') || pos.includes('上什') || pos.includes('打馕')) role = '后厨'
|
||||
else if (pos.includes('兼职') || pos.includes('小时工')) role = '兼职'
|
||||
|
||||
if (!staffing[store]) staffing[store] = {}
|
||||
for (let h = clock.start; h <= clock.end; h++) {
|
||||
if (!staffing[store][h]) staffing[store][h] = { 前厅: 0, 后厨: 0, 管理: 0, 其他: 0 }
|
||||
staffing[store][h][role as '前厅' | '后厨' | '管理' | '其他']++
|
||||
}
|
||||
}
|
||||
|
||||
// 转为数组输出
|
||||
const output: any[] = []
|
||||
for (const [store, hours] of Object.entries(staffing)) {
|
||||
for (let h = 0; h < 24; h++) {
|
||||
const s = hours[h] || { 前厅: 0, 后厨: 0, 管理: 0, 其他: 0 }
|
||||
output.push({
|
||||
store_name: store,
|
||||
hour: h,
|
||||
front: s.前厅,
|
||||
kitchen: s.后厨,
|
||||
management: s.管理,
|
||||
other: s.其他,
|
||||
total: s.前厅 + s.后厨 + s.管理 + s.其他,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sendSuccess(res, output)
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
})
|
||||
|
||||
// 工作日vs周末客流
|
||||
router.get('/dow-traffic', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user