初始化:连锁餐饮数字化运营管理平台

This commit is contained in:
freedakgmail
2026-07-26 22:48:08 +08:00
commit a7874d79b5
67 changed files with 14391 additions and 0 deletions
+117
View File
@@ -0,0 +1,117 @@
import { useQuery } from '@tanstack/react-query'
import { useNavigate } from 'react-router-dom'
import api from '@/lib/api'
import { Badge } from '@/components/Badge'
import { DataTable } from '@/components/DataTable'
import { MetricCard } from '@/components/MetricCard'
import { Pagination } from '@/components/Pagination'
import { formatCurrency, formatPercent, riskColor } from '@/lib/utils'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { useState, useMemo } from 'react'
const PAGE_SIZE = 10
export function RegionalPage() {
const navigate = useNavigate()
const [riskFilter, setRiskFilter] = useState('')
const [riskPage, setRiskPage] = useState(1)
const [checkPage, setCheckPage] = useState(1)
const { data: riskData, isLoading: riskLoading } = useQuery({
queryKey: ['stores/risk'],
queryFn: () => api.get('/stores/risk'),
})
const { data: weeklyData, isLoading: weeklyLoading } = useQuery({
queryKey: ['tasks/weekly-check'],
queryFn: () => api.get('/tasks/weekly-check', { params: { month: '2026-05' } }),
})
const { data: priorityData, isLoading: priorityLoading } = useQuery({
queryKey: ['stores/priority'],
queryFn: () => api.get('/stores/priority'),
})
const riskRows = ((riskData as any)?.data || []).filter((r: any) => !riskFilter || r.risk_level === riskFilter)
const weeklyRows = (weeklyData as any)?.data || []
const priorityRows = (priorityData as any)?.data || []
const p0p1 = priorityRows.filter((s: any) => s.action_priority?.startsWith('P0') || s.action_priority?.startsWith('P1'))
const needCheck = weeklyRows.filter((w: any) => parseInt(w.consecutive_no_improve_weeks) >= 2)
const pagedRiskRows = useMemo(() => riskRows.slice((riskPage - 1) * PAGE_SIZE, riskPage * PAGE_SIZE), [riskRows, riskPage])
const pagedNeedCheck = useMemo(() => needCheck.slice((checkPage - 1) * PAGE_SIZE, checkPage * PAGE_SIZE), [needCheck, checkPage])
const pageLoading = riskLoading || weeklyLoading || priorityLoading
if (pageLoading) {
return <LoadingSpinner text="加载区域经理工作台..." />
}
return (
<div className="space-y-4">
<h1 className="text-xl font-bold"></h1>
{/* 汇总卡片 */}
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
<MetricCard title="红黄门店数" value={riskRows.filter((r: any) => r.risk_level !== '绿色').length} />
<MetricCard title="P0+P1门店" value={p0p1.length} />
<MetricCard title="需到店检查" value={needCheck.length} />
<MetricCard title="周检待录入" value={weeklyRows.filter((w: any) => !w.last_check_date).length} />
</div>
{/* 风险筛选 */}
<div className="flex gap-2">
{['', '红色', '黄色', '绿色'].map((r) => (
<button
key={r || 'all'}
onClick={() => setRiskFilter(r)}
className={`rounded-md border px-3 py-1.5 text-sm ${riskFilter === r ? 'border-primary bg-primary text-primary-foreground' : ''}`}
>
{r || '全部'}
</button>
))}
</div>
{/* 门店红黄绿列表 */}
<div className="rounded-lg border bg-card p-4">
<h2 className="mb-3 text-sm font-bold"> ({riskRows.length})</h2>
<Pagination page={riskPage} pageSize={PAGE_SIZE} total={riskRows.length} onPageChange={setRiskPage} />
<div className="mt-3">
<DataTable
columns={[
{ key: 'store_name', label: '门店' },
{ key: 'risk_level', label: '风险', render: (r) => <Badge type="risk" text={r.risk_level} /> },
{ key: 'received', label: '实收', align: 'right', render: (r) => formatCurrency(r.received) },
{ key: 'discount_rate_pct', label: '优惠率', align: 'right', render: (r) => formatPercent(r.discount_rate_pct) },
{ key: 'theoretical_margin_pct', label: '毛利率', align: 'right', render: (r) => formatPercent(r.theoretical_margin_pct) },
{ key: 'anomaly_rate_pct', label: '异常率', align: 'right', render: (r) => formatPercent(r.anomaly_rate_pct) },
]}
data={pagedRiskRows}
onRowClick={(r) => navigate(`/stores/${r.store_code}`)}
/>
</div>
</div>
{/* 需到店检查门店 */}
{needCheck.length > 0 && (
<div className="rounded-lg border bg-card p-4">
<h2 className="mb-3 text-sm font-bold text-red-600">2({needCheck.length})</h2>
<Pagination page={checkPage} pageSize={PAGE_SIZE} total={needCheck.length} onPageChange={setCheckPage} />
<div className="mt-3">
<DataTable
columns={[
{ key: 'store_name', label: '门店' },
{ key: 'problem_indicator', label: '问题指标' },
{ key: 'consecutive_no_improve_weeks', label: '连续未改善周数', align: 'center' },
{ key: 'status', label: '任务状态', render: (r) => <Badge type="status" text={r.status} /> },
]}
data={pagedNeedCheck}
onRowClick={(r) => navigate(`/tasks/${r.task_id}`)}
/>
</div>
</div>
)}
</div>
)
}