feat: 智能排班人员预测系统

- 新增人员预测tab,基于多维度产能指标生成招聘/优化建议
- 规则引擎:7条优化规则 + 7条招聘规则 + 4步决策逻辑
- 动态标准值:基于全部门店中位数计算,替代硬编码阈值
- 互斥逻辑:有优化信号时抑制所有招聘信号
- 占比超配时不触发招聘信号(R9/R11/R12/R13)
- 离职缺口需出勤不足才触发招聘(R10)
- 管理岗满编时不触发出勤不足招聘(R9)
- 前端可折叠规则引擎面板,展示全部规则和触发条件
- 产能指标对比展示实际值 vs 中位数标准
This commit is contained in:
freedakgmail
2026-07-29 23:41:56 +08:00
parent 80d67ccb73
commit 75a483bfdb
17 changed files with 3497 additions and 3 deletions
@@ -0,0 +1,114 @@
import { useQuery } from '@tanstack/react-query'
import api from '@/lib/api'
import { LoadingSpinner } from '@/components/LoadingSpinner'
const LEVEL_STYLES: Record<string, { bg: string; border: string; text: string; icon: string }> = {
red: { bg: 'bg-red-50', border: 'border-red-200', text: 'text-red-700', icon: '🔴' },
orange: { bg: 'bg-orange-50', border: 'border-orange-200', text: 'text-orange-700', icon: '🟠' },
yellow: { bg: 'bg-yellow-50', border: 'border-yellow-200', text: 'text-yellow-700', icon: '🟡' },
green: { bg: 'bg-green-50', border: 'border-green-200', text: 'text-green-700', icon: '🟢' },
}
const CATEGORY_LABELS: Record<string, string> = {
'人效': '人效分析',
'成本': '成本管控',
'加班': '加班管理',
'客流': '客流规律',
'考勤': '考勤管理',
'离职': '人员稳定',
}
export function OverallAnalysisTab() {
const { data, isLoading } = useQuery({
queryKey: ['ss/overall-analysis'],
queryFn: () => api.get('/smart-scheduling/overall-analysis'),
})
if (isLoading) return <LoadingSpinner text="生成总体智能分析..." />
const result = (data as any)?.data || {}
const kpis: any[] = result.kpis || []
const insights: any[] = result.insights || []
const redCount = insights.filter(i => i.level === 'red').length
const orangeCount = insights.filter(i => i.level === 'orange').length
const yellowCount = insights.filter(i => i.level === 'yellow').length
const greenCount = insights.filter(i => i.level === 'green').length
return (
<div className="space-y-4">
{/* KPI 卡片 */}
<div className="grid grid-cols-5 gap-3">
{kpis.map((k) => (
<div key={k.label} className="rounded-lg border p-3 text-center">
<p className="text-xs text-muted-foreground">{k.label}</p>
<p className="text-xl font-bold mt-1">
{k.value}
<span className="text-xs font-normal text-muted-foreground ml-1">{k.unit}</span>
</p>
</div>
))}
</div>
{/* 预警汇总 */}
<div className="grid grid-cols-4 gap-3">
<div className="rounded-lg border border-red-200 bg-red-50 p-3">
<p className="text-xs text-red-600"></p>
<p className="text-2xl font-bold text-red-700">{redCount}</p>
</div>
<div className="rounded-lg border border-orange-200 bg-orange-50 p-3">
<p className="text-xs text-orange-600"></p>
<p className="text-2xl font-bold text-orange-700">{orangeCount}</p>
</div>
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-3">
<p className="text-xs text-yellow-600"></p>
<p className="text-2xl font-bold text-yellow-700">{yellowCount}</p>
</div>
<div className="rounded-lg border border-green-200 bg-green-50 p-3">
<p className="text-xs text-green-600"></p>
<p className="text-2xl font-bold text-green-700">{greenCount}</p>
</div>
</div>
{/* 智能诊断建议 */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-lg font-bold"></h2>
<span className="text-xs text-muted-foreground"></span>
</div>
{insights.length === 0 ? (
<div className="rounded-lg border p-8 text-center text-muted-foreground">
</div>
) : (
<div className="space-y-3">
{insights.map((ins, i) => {
const style = LEVEL_STYLES[ins.level] || LEVEL_STYLES.yellow
return (
<div key={i} className={`rounded-lg border ${style.border} ${style.bg} p-4`}>
<div className="flex items-start gap-3">
<span className="text-lg mt-0.5">{style.icon}</span>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className={`text-xs px-2 py-0.5 rounded bg-white/60 ${style.text} font-medium`}>
{CATEGORY_LABELS[ins.category] || ins.category}
</span>
<h3 className="font-bold text-sm">{ins.title}</h3>
</div>
<p className="text-sm text-muted-foreground mb-2">{ins.detail}</p>
<div className="flex items-start gap-2">
<span className="text-xs text-muted-foreground whitespace-nowrap">💡 </span>
<p className="text-sm">{ins.suggestion}</p>
</div>
</div>
</div>
</div>
)
})}
</div>
)}
</div>
</div>
)
}