fix: P2/P3审计问题修复 + 更新审计文档移除已修复问题
- 修复schema前缀:确认central_kitchen/mv_distribution/mv_dish_sales等表在public schema,撤销错误的analytics.前缀 - 移除API内部物化视图刷新:mv_distribution_monthly/mv_dish_sales_monthly改为console.warn - 修复BOM树节点key:使用path字段替代material_code+level确保唯一性 - 修复HR人才矩阵potential_score:基于v3_learning_record动态计算替代硬编码60 - 修复StoreDetailPage公司均值:优先使用API返回的companyAvg - 修复前端字段名匹配:discount_rate_pct/check_comment等 - 添加explicit column list和LIMIT到大型查询 - 更新数据溯源审计.md:移除全部52个已修复/确认无需修复的问题条目
This commit is contained in:
@@ -31,7 +31,7 @@ export function ExpenseOverviewTab({ month }: { month: string }) {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<CollapsibleSection title="费用结构分析" subtitle="2026年4月各费用科目占比">
|
||||
<CollapsibleSection title="费用结构分析" subtitle={`${month}各费用科目占比`}>
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<PieChart>
|
||||
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={120} labelLine={false}>
|
||||
|
||||
@@ -116,7 +116,7 @@ function StoreDetailModal({ store, onClose }: { store: StoreDetail | null, onClo
|
||||
{ label: '面积(㎡)', value: formatNumber(store.area_sqm) },
|
||||
{ label: '坪效', value: formatNumber(store.received_per_sqm) },
|
||||
{ label: '账单数', value: formatNumber(store.bill_count) },
|
||||
{ label: '租约到期', value: store.lease_expiry_date ? new Date(store.lease_expiry_date).toLocaleDateString('zh-CN') : '-' },
|
||||
{ label: '租约到期', value: store.lease_expiry_date ? String(store.lease_expiry_date).substring(0, 10) : '-' },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -197,9 +197,9 @@ export function LossDiagnosisTab({ month }: { month: string }) {
|
||||
const rows = (data as any)?.data || []
|
||||
const meta = (data as any)?.meta || { page, pageSize: PAGE_SIZE, total: 0 }
|
||||
|
||||
const p0Count = rows.filter((r: any) => r.priority === 'P0').length
|
||||
const p1Count = rows.filter((r: any) => r.priority === 'P1').length
|
||||
const totalLoss = rows.reduce((sum: number, r: any) => sum + parseFloat(r.actual_store_contribution || 0), 0)
|
||||
const p0Count = meta.evalStats ? Number(meta.evalStats.p0_count) : rows.filter((r: any) => r.priority === 'P0').length
|
||||
const p1Count = meta.evalStats ? Number(meta.evalStats.p1_count) : rows.filter((r: any) => r.priority === 'P1').length
|
||||
const totalLoss = meta.evalStats ? Number(meta.evalStats.total_loss) : rows.reduce((sum: number, r: any) => sum + parseFloat(r.actual_store_contribution || 0), 0)
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
|
||||
@@ -76,7 +76,7 @@ export function RentRiskTab({ month }: { month: string }) {
|
||||
{ key: 'rent_rate_pct', label: '租金率', align: 'right', render: (r) => <span className={parseFloat(r.rent_rate_pct) > 20 ? 'text-red-600 font-medium' : ''}>{formatPercent(r.rent_rate_pct)}</span> },
|
||||
{ key: 'area_sqm', label: '面积(㎡)', align: 'right', render: (r) => formatNumber(r.area_sqm) },
|
||||
{ key: 'received_per_sqm', label: '坪效', align: 'right', render: (r) => formatNumber(r.received_per_sqm) },
|
||||
{ key: 'lease_expiry_date', label: '租约到期', render: (r) => r.lease_expiry_date ? new Date(r.lease_expiry_date).toLocaleDateString('zh-CN') : '-' },
|
||||
{ key: 'lease_expiry_date', label: '租约到期', render: (r) => r.lease_expiry_date ? String(r.lease_expiry_date).substring(0, 10) : '-' },
|
||||
{ key: 'risk_level', label: '风险等级', render: (r) => <span className={RISK_COLORS[r.risk_level] || ''}>{r.risk_level}</span> },
|
||||
{ key: 'suggestion', label: '建议' },
|
||||
]}
|
||||
|
||||
@@ -59,7 +59,7 @@ function EvalDetailModal({ store, onClose }: { store: EvalStoreDetail | null, on
|
||||
{ label: '租金率', value: formatPercent(store.rent_rate_pct), warn: parseFloat(store.rent_rate_pct) > 20 },
|
||||
{ label: '坪效', value: formatNumber(store.received_per_sqm), warn: parseFloat(store.received_per_sqm) < 1000 },
|
||||
{ label: '面积(㎡)', value: formatNumber(store.area_sqm) },
|
||||
{ label: '租约到期', value: store.lease_expiry_date ? new Date(store.lease_expiry_date).toLocaleDateString('zh-CN') : '-' },
|
||||
{ label: '租约到期', value: store.lease_expiry_date ? String(store.lease_expiry_date).substring(0, 10) : '-' },
|
||||
]
|
||||
|
||||
return (
|
||||
@@ -167,7 +167,7 @@ export function StoreEvaluationTab({ month }: { month: string }) {
|
||||
{ key: 'actual_store_contribution', label: '贡献利润', align: 'right', render: (r) => <span className={parseFloat(r.actual_store_contribution) < 0 ? 'text-red-600 font-medium' : 'text-green-600'}>{formatCurrency(r.actual_store_contribution)}</span> },
|
||||
{ key: 'actual_store_contribution_rate_pct', label: '贡献率', align: 'right', render: (r) => formatPercent(r.actual_store_contribution_rate_pct) },
|
||||
{ key: 'received_per_sqm', label: '坪效', align: 'right', render: (r) => formatNumber(r.received_per_sqm) },
|
||||
{ key: 'lease_expiry_date', label: '租约到期', render: (r) => r.lease_expiry_date ? new Date(r.lease_expiry_date).toLocaleDateString('zh-CN') : '-' },
|
||||
{ key: 'lease_expiry_date', label: '租约到期', render: (r) => r.lease_expiry_date ? String(r.lease_expiry_date).substring(0, 10) : '-' },
|
||||
{ key: 'evaluation_detail', label: '评估详情', render: (r) => <span className="text-xs">{r.evaluation_detail}</span> },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -45,16 +45,15 @@ function buildTree(flatTree: any[]): BomTreeNode[] {
|
||||
const roots: BomTreeNode[] = []
|
||||
flatTree.forEach((n: any) => {
|
||||
const node: BomTreeNode = { ...n, children: [] }
|
||||
nodeMap.set(node.material_code + '_' + node.level, node)
|
||||
nodeMap.set(node.path, node)
|
||||
})
|
||||
flatTree.forEach((n: any) => {
|
||||
const key = n.material_code + '_' + n.level
|
||||
const node = nodeMap.get(key)!
|
||||
const node = nodeMap.get(n.path)!
|
||||
if (n.level === 1) {
|
||||
roots.push(node)
|
||||
} else {
|
||||
const parentKey = n.parent_material_code + '_' + (n.level - 1)
|
||||
const parent = nodeMap.get(parentKey)
|
||||
const parentPath = n.path.split(' -> ').slice(0, -1).join(' -> ')
|
||||
const parent = nodeMap.get(parentPath)
|
||||
if (parent) parent.children.push(node)
|
||||
else roots.push(node)
|
||||
}
|
||||
@@ -176,7 +175,7 @@ export function BomPenetrationPage() {
|
||||
const childrenToRender = hasChildren ? node.children : (lazyLoaded || [])
|
||||
const isZero = node.theoretical_qty === 0 && node.issue_qty === 0 && node.theoretical_amt === 0 && node.issue_amt === 0
|
||||
rows.push(
|
||||
<tr key={node.material_code + '_' + node.level} className={cn('border-b hover:bg-muted/50', node.is_finished_product && 'bg-purple-50/30', isZero && 'bg-yellow-50/40')}>
|
||||
<tr key={node.path} className={cn('border-b hover:bg-muted/50', node.is_finished_product && 'bg-purple-50/30', isZero && 'bg-yellow-50/40')}>
|
||||
<td className="py-1.5 pr-3" style={{ paddingLeft: `${depth * 20 + 8}px` }}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{showChevron ? (
|
||||
|
||||
@@ -55,7 +55,7 @@ export function RegionalPage() {
|
||||
const pageLoading = riskLoading || weeklyLoading || priorityLoading
|
||||
|
||||
const weeklyMutation = useMutation({
|
||||
mutationFn: ({ id, text }: { id: number; text: string }) => api.put(`/tasks/${id}/weekly-check`, { weekly_comment: text }),
|
||||
mutationFn: ({ id, text }: { id: number; text: string }) => api.put(`/tasks/${id}/weekly-check`, { check_comment: text }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tasks/weekly-check'] })
|
||||
setWeeklyModal(null)
|
||||
|
||||
@@ -68,7 +68,7 @@ export function RevenuePage() {
|
||||
received: Number(r.received || 0),
|
||||
bills: Number(r.bill_count || 0),
|
||||
avg_bill: Number(r.avg_bill_value || 0),
|
||||
discount_rate: Number(r.avg_discount_rate_pct || 0),
|
||||
discount_rate: Number(r.discount_rate_pct || 0),
|
||||
}))
|
||||
|
||||
// 环比
|
||||
|
||||
@@ -47,7 +47,7 @@ export function StoreDetailPage() {
|
||||
|
||||
const { data: healthData } = useQuery({
|
||||
queryKey: ['store-health', code, month],
|
||||
queryFn: () => api.get('/situational-awareness/health-score', { params: { month } }),
|
||||
queryFn: () => api.get('/situational-awareness/health-score', { params: { month, store_code: code } }),
|
||||
})
|
||||
|
||||
const { data: dailyData } = useQuery({
|
||||
@@ -125,8 +125,8 @@ export function StoreDetailPage() {
|
||||
const quadrant = action?.management_quadrant || ''
|
||||
const quadrantInfo = quadrantDesc[quadrant]
|
||||
|
||||
// 公司均值
|
||||
const companyAvg = { daily_rev: 20144, bill: 36.8, discount: 20.5, margin: 71.5, member: 15.9, repeat: 37.0, delivery: 36.1, combo: 20.6, items: 4.05 }
|
||||
// 公司均值(从API获取,fallback到默认值)
|
||||
const companyAvg = sd.companyAvg || { daily_rev: 20144, bill: 36.8, discount: 20.5, margin: 71.5, member: 15.9, repeat: 37.0, delivery: 36.1, combo: 20.6, items: 4.05 }
|
||||
|
||||
// 偏差项明细
|
||||
const deviations: { label: string; store: number; company: number; unit: string; good: 'high' | 'low' }[] = [
|
||||
|
||||
@@ -8,6 +8,7 @@ import { formatCurrency, formatNumber, formatPercent } from '@/lib/utils'
|
||||
import { useState } from 'react'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, PieChart, Pie, Cell, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, Radar } from 'recharts'
|
||||
import { Tabs } from '@/components/Tabs'
|
||||
import { MonthPicker } from '@/components/MonthPicker'
|
||||
|
||||
const GRADE_COLORS: Record<string, string> = { A: '#22c55e', B: '#3b82f6', C: '#eab308', D: '#ef4444' }
|
||||
const RISK_COLORS: Record<string, string> = { '红色': '#ef4444', '黄色': '#eab308', '绿色': '#22c55e' }
|
||||
@@ -20,18 +21,18 @@ export function StoreGradePage() {
|
||||
|
||||
const { data: gradeData, isLoading: gradeLoading } = useQuery({
|
||||
queryKey: ['store-grade/grades', month],
|
||||
queryFn: () => api.get('/store-grade/grades', { params: { month: month + '-01' } }),
|
||||
queryFn: () => api.get('/store-grade/grades', { params: { month } }),
|
||||
enabled: tab === 'grade',
|
||||
})
|
||||
|
||||
const { data: closureData, isLoading: closureLoading } = useQuery({
|
||||
queryKey: ['store-grade/closure-analysis', month],
|
||||
queryFn: () => api.get('/store-grade/closure-analysis', { params: { month: month + '-01' } }),
|
||||
queryFn: () => api.get('/store-grade/closure-analysis', { params: { month } }),
|
||||
enabled: tab === 'closure',
|
||||
})
|
||||
|
||||
const autoGradeMutation = useMutation({
|
||||
mutationFn: () => api.post('/store-grade/auto-grade', null, { params: { month: month + '-01' } }),
|
||||
mutationFn: () => api.post('/store-grade/auto-grade', null, { params: { month } }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['store-grade/grades'] })
|
||||
},
|
||||
@@ -89,12 +90,7 @@ export function StoreGradePage() {
|
||||
<p className="mt-0.5 text-xs text-muted-foreground">自动分级 · 风险监控 · 关停测算 · {month}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="month"
|
||||
value={month}
|
||||
onChange={(e) => setMonth(e.target.value)}
|
||||
className="rounded border px-2 py-1 text-sm"
|
||||
/>
|
||||
<MonthPicker month={month} onChange={setMonth} />
|
||||
{tab === 'grade' && (
|
||||
<button
|
||||
onClick={() => autoGradeMutation.mutate()}
|
||||
|
||||
Reference in New Issue
Block a user