feat: 客流热力图增加在岗人员行+规则引擎面板+菜单名称调整

- 热力图每门店增加在岗人员行(前厅/后厨/管理/其他),从考勤打卡记录解析
- 新增traffic-heatmap-staffing API,解析考勤打卡时间计算每小时在岗人数
- 人员预测tab新增可折叠规则引擎面板,展示全部规则和触发条件
- 后端API返回ruleEngine字段(动态标准+优化规则+招聘规则+决策逻辑)
- R9/R11/R12/R13增加占比超配约束,过滤不合理招聘建议
- 菜单项'菜品成本分析'→'菜品成本','门店费用分析'→'门店费用'
- 热力图横向滚动条始终可见
This commit is contained in:
freedakgmail
2026-07-30 00:04:54 +08:00
parent 75a483bfdb
commit 6bab0ad84a
5 changed files with 146 additions and 14 deletions
+2 -2
View File
@@ -37,8 +37,8 @@ const menuGroups: MenuGroup[] = [
items: [
{ path: '/sku', label: '商品SKU', icon: Package, roles: ['hq', 'dept'] },
{ path: '/cost', label: '成本库存', icon: DollarSign, roles: ['hq', 'dept'] },
{ path: '/cost-analysis', label: '菜品成本分析', icon: PieChart, roles: ['hq', 'dept'] },
{ path: '/store-expense', label: '门店费用分析', icon: Wallet, roles: ['hq', 'dept'] },
{ path: '/cost-analysis', label: '菜品成本', icon: PieChart, roles: ['hq', 'dept'] },
{ path: '/store-expense', label: '门店费用', icon: Wallet, roles: ['hq', 'dept'] },
{ path: '/smart-scheduling', label: '智能排班', icon: CalendarClock, roles: ['hq', 'dept', 'regional'] },
{ path: '/platform', label: '平台优惠', icon: ShoppingBag, roles: ['hq', 'dept'] },
{ path: '/member', label: '会员复购', icon: Users, roles: ['hq', 'dept'] },
@@ -1,4 +1,4 @@
import { useState, useMemo } from 'react'
import { useState, useMemo, Fragment } from 'react'
import { useQuery } from '@tanstack/react-query'
import api from '@/lib/api'
import { CollapsibleSection } from '@/components/CollapsibleSection'
@@ -30,6 +30,11 @@ export function TrafficHeatmapTab() {
queryFn: () => api.get(`/smart-scheduling/traffic-heatmap${storeName ? `?store=${storeName}` : ''}`),
})
const { data: staffing } = useQuery({
queryKey: ['ss/traffic-heatmap-staffing', storeName],
queryFn: () => api.get(`/smart-scheduling/traffic-heatmap-staffing${storeName ? `?store=${storeName}` : ''}`),
})
const { data: mealPeriod } = useQuery({
queryKey: ['ss/meal-period-traffic', storeName],
queryFn: () => api.get(`/smart-scheduling/meal-period-traffic${storeName ? `?store=${storeName}` : ''}`),
@@ -38,6 +43,7 @@ export function TrafficHeatmapTab() {
const stores = (overview as any)?.data || []
const heatRows = (heatmap as any)?.data || []
const mealRows = (mealPeriod as any)?.data || []
const staffingRows = (staffing as any)?.data || []
const storeNames = Array.from(new Set(stores.map((s: any) => s.store_name as string))) as string[]
@@ -48,6 +54,13 @@ export function TrafficHeatmapTab() {
heatMap[r.store_name][r.hour] = r.bills
})
// 人员分布: store -> hour -> { front, kitchen, management, other, total }
const staffingMap: Record<string, Record<number, { front: number; kitchen: number; management: number; other: number; total: number }>> = {}
staffingRows.forEach((r: any) => {
if (!staffingMap[r.store_name]) staffingMap[r.store_name] = {}
staffingMap[r.store_name][r.hour] = { front: r.front, kitchen: r.kitchen, management: r.management, other: r.other, total: r.total }
})
function heatColor(bills: number, max: number): string {
if (!bills) return ''
const ratio = bills / max
@@ -127,25 +140,40 @@ export function TrafficHeatmapTab() {
{storeNames.map((s) => <option key={s} value={s}>{s}</option>)}
</select>
<span className="text-xs text-muted-foreground"></span>
<span className="text-xs text-muted-foreground ml-2">: <span className="text-blue-600"></span>/<span className="text-orange-600"></span>/<span className="text-purple-600"></span>/<span className="text-gray-500"></span></span>
</div>
{isLoading ? <LoadingSpinner text="加载客流热力图..." /> : (
<div className="overflow-x-auto">
<table className="text-xs">
<div className="overflow-x-auto -mx-4 px-4 heatmap-scroll">
<table className="text-xs whitespace-nowrap">
<thead>
<tr>
<th className="px-2 py-1 text-left sticky left-0 bg-card"></th>
{hours.map(h => <th key={h} className="px-1 py-1 text-center min-w-[28px]">{h}</th>)}
{hours.map(h => <th key={h} className="px-1 py-1 text-center min-w-[32px]">{h}</th>)}
</tr>
</thead>
<tbody>
{Object.keys(heatMap).map(store => (
<tr key={store}>
<td className="px-2 py-1 whitespace-nowrap sticky left-0 bg-card">{store}</td>
{hours.map(h => {
const bills = heatMap[store]?.[h] || 0
return <td key={h} className={`px-1 py-1 text-center ${heatColor(bills, maxBills)}`}>{bills > 0 ? bills : ''}</td>
})}
</tr>
<Fragment key={store}>
<tr>
<td className="px-2 py-1 whitespace-nowrap sticky left-0 bg-card font-medium">{store}</td>
{hours.map(h => {
const bills = heatMap[store]?.[h] || 0
return <td key={h} className={`px-1 py-1 text-center ${heatColor(bills, maxBills)}`}>{bills > 0 ? bills : ''}</td>
})}
</tr>
<tr key={store + '-staff'} className="border-b">
<td className="px-2 py-0.5 whitespace-nowrap sticky left-0 bg-card text-xs text-muted-foreground"></td>
{hours.map(h => {
const s = staffingMap[store]?.[h]
if (!s || s.total === 0) return <td key={h} className="px-1 py-0.5 text-center text-xs text-muted-foreground/40 min-w-[42px]">-</td>
return (
<td key={h} className="px-1 py-0.5 text-center text-xs min-w-[42px]">
<span className="text-blue-600">{s.front}</span><span className="text-muted-foreground">/</span><span className="text-orange-600">{s.kitchen}</span><span className="text-muted-foreground">/</span><span className="text-purple-600">{s.management}</span><span className="text-muted-foreground">/</span><span className="text-gray-500">{s.other}</span>
</td>
)
})}
</tr>
</Fragment>
))}
</tbody>
</table>