Files
SBrainCO/client/src/components/smart-scheduling/SchedulingSuggestionTab.tsx
T

163 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import api from '@/lib/api'
import { CollapsibleSection } from '@/components/CollapsibleSection'
import { FilterableTable } from '@/components/FilterableTable'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { formatNumber } from '@/lib/utils'
const ACTION_COLORS: Record<string, string> = {
'需增配': 'text-red-600 font-medium',
'可减配': 'text-blue-500 font-medium',
'配置合理': 'text-green-600',
}
const ROW_BG: Record<string, string> = {
'需增配': 'bg-red-50',
'可减配': 'bg-blue-50',
'配置合理': 'bg-green-50',
}
function gapLabel(suggested: any, current: any): string {
const s = parseFloat(suggested) || 0
const c = parseFloat(current) || 0
const gap = s - c
if (gap > 0) return `+${gap.toFixed(1)}`
if (gap < 0) return gap.toFixed(1)
return '0'
}
function gapColor(suggested: any, current: any): string {
const s = parseFloat(suggested) || 0
const c = parseFloat(current) || 0
const gap = s - c
if (gap > 0) return 'text-red-600 font-medium'
if (gap < 0) return 'text-blue-500'
return 'text-muted-foreground'
}
const tableColumns = [
{ key: 'hour', label: '时段', render: (r: any) => `${r.hour}:00` },
{ key: 'avg_daily_bills', label: '日均账单', align: 'right' as const, render: (r: any) => formatNumber(r.avg_daily_bills) },
{ key: 'p85_bills', label: '高峰账单', align: 'right' as const, render: (r: any) => <span className="text-orange-600">{formatNumber(r.p85_bills)}</span> },
{ key: 'volatility', label: '波动系数', align: 'right' as const, render: (r: any) => <span className={parseFloat(r.volatility) > 1 ? 'text-red-600 font-medium' : ''}>{r.volatility}</span> },
{ key: 'suggested_front', label: '建议前厅', align: 'right' as const, render: (r: any) => r.suggested_front },
{ key: 'current_front', label: '当前前厅', align: 'right' as const, render: (r: any) => r.current_front },
{ key: 'gap_front', label: '前厅缺口', align: 'right' as const, render: (r: any) => <span className={gapColor(r.suggested_front, r.current_front)}>{gapLabel(r.suggested_front, r.current_front)}</span> },
{ key: 'suggested_kitchen', label: '建议后厨', align: 'right' as const, render: (r: any) => r.suggested_kitchen },
{ key: 'current_kitchen', label: '当前后厨', align: 'right' as const, render: (r: any) => r.current_kitchen },
{ key: 'gap_kitchen', label: '后厨缺口', align: 'right' as const, render: (r: any) => <span className={gapColor(r.suggested_kitchen, r.current_kitchen)}>{gapLabel(r.suggested_kitchen, r.current_kitchen)}</span> },
{ key: 'suggested_manager', label: '建议管理', align: 'right' as const, render: (r: any) => r.suggested_manager },
{ key: 'current_other', label: '当前管理', align: 'right' as const, render: (r: any) => r.current_other },
{ key: 'gap_manager', label: '管理缺口', align: 'right' as const, render: (r: any) => <span className={gapColor(r.suggested_manager, r.current_other)}>{gapLabel(r.suggested_manager, r.current_other)}</span> },
{ key: 'suggested_total', label: '建议合计', align: 'right' as const, render: (r: any) => <span className="font-medium">{r.suggested_total}</span> },
{ key: 'current_total', label: '当前合计', align: 'right' as const, render: (r: any) => r.current_total },
{ key: 'staff_gap', label: '总缺口', align: 'right' as const, render: (r: any) => <span className={parseFloat(r.staff_gap) > 0 ? 'text-red-600 font-bold' : parseFloat(r.staff_gap) < 0 ? 'text-blue-500 font-bold' : ''}>{parseFloat(r.staff_gap) > 0 ? '+' : ''}{r.staff_gap}</span> },
{ key: 'action', label: '建议', render: (r: any) => <span className={ACTION_COLORS[r.action] || ''}>{r.action}</span> },
]
export function SchedulingSuggestionTab({ month }: { month: string }) {
const { data: storesData } = useQuery({
queryKey: ['ss/stores'],
queryFn: () => api.get('/smart-scheduling/stores'),
})
const stores = (storesData as any)?.data || []
const [storeName, setStoreName] = useState('七里庄店')
const [frontTarget, setFrontTarget] = useState(15)
const [kitchenTarget, setKitchenTarget] = useState(25)
const { data, isLoading } = useQuery({
queryKey: ['ss/suggestion', storeName, frontTarget, kitchenTarget, month],
queryFn: () => api.get(`/smart-scheduling/scheduling-suggestion?store=${storeName}&front_target=${frontTarget}&kitchen_target=${kitchenTarget}&month=${month}`),
enabled: !!storeName,
})
const rows = (data as any)?.data || []
const workdayRows = rows.filter((r: any) => r.day_type === '工作日')
const weekendRows = rows.filter((r: any) => r.day_type === '周末')
const increaseCount = rows.filter((r: any) => r.action === '需增配').length
const decreaseCount = rows.filter((r: any) => r.action === '可减配').length
const okCount = rows.filter((r: any) => r.action === '配置合理').length
return (
<div className="space-y-4">
<CollapsibleSection title="排班建议" subtitle="基于历史客流规律,分岗位推荐各时段在岗人数">
<div className="mb-3 flex gap-3 items-center flex-wrap">
<div className="flex gap-2 items-center">
<label className="text-sm text-muted-foreground"></label>
<select value={storeName} onChange={(e) => setStoreName(e.target.value)} className="border rounded px-2 py-1 text-sm">
{stores.map((s: any) => <option key={s.store_name} value={s.store_name}>{s.store_name}</option>)}
</select>
</div>
<div className="flex gap-2 items-center">
<label className="text-sm text-muted-foreground"></label>
<input type="number" value={frontTarget} onChange={(e) => setFrontTarget(parseInt(e.target.value) || 15)} className="border rounded px-2 py-1 text-sm w-16" />
<span className="text-xs text-muted-foreground">//</span>
</div>
<div className="flex gap-2 items-center">
<label className="text-sm text-muted-foreground"></label>
<input type="number" value={kitchenTarget} onChange={(e) => setKitchenTarget(parseInt(e.target.value) || 25)} className="border rounded px-2 py-1 text-sm w-16" />
<span className="text-xs text-muted-foreground">//</span>
</div>
</div>
{isLoading ? <LoadingSpinner text="生成排班建议..." /> : (
<>
<div className="grid grid-cols-3 gap-3 mb-4">
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="text-xl font-bold text-red-600">{increaseCount}</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="text-xl font-bold text-blue-500">{decreaseCount}</p>
</div>
<div className="rounded-lg border p-3">
<p className="text-xs text-muted-foreground"></p>
<p className="text-xl font-bold text-green-600">{okCount}</p>
</div>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm font-bold mb-2"></h3>
<FilterableTable
data={workdayRows}
sortOptions={[
{ key: 'hour', label: '时段' },
{ key: 'avg_daily_bills', label: '日均账单' },
{ key: 'staff_gap', label: '总缺口' },
]}
defaultSort="hour"
defaultOrder="asc"
filterKey="action"
filterLabel="全部建议"
columns={tableColumns}
/>
</div>
<div>
<h3 className="text-sm font-bold mb-2"></h3>
<FilterableTable
data={weekendRows}
sortOptions={[
{ key: 'hour', label: '时段' },
{ key: 'avg_daily_bills', label: '日均账单' },
{ key: 'staff_gap', label: '总缺口' },
]}
defaultSort="hour"
defaultOrder="asc"
filterKey="action"
filterLabel="全部建议"
columns={tableColumns}
/>
</div>
</div>
</>
)}
</CollapsibleSection>
</div>
)
}