feat: 配送物化视图优化查询性能 + 饼图统一处理 + SKU长尾治理明细 + 地图初始聚焦北京

This commit is contained in:
freedakgmail
2026-08-02 08:53:21 +08:00
parent ed7beb7e47
commit 73f12f40d1
20 changed files with 288 additions and 103 deletions
+9 -4
View File
@@ -47,11 +47,16 @@ interface StoreMapProps {
function FitBounds({ points }: { points: [number, number][] }) {
const map = useMap()
useEffect(() => {
if (points.length === 0) return
if (points.length === 1) {
map.setView(points[0], 13)
// 只适配北京区域内的点,避免外地门店导致地图缩放过远
const beijingPoints = points.filter(([lat, lng]) =>
lat >= 39.4 && lat <= 41.1 && lng >= 115.4 && lng <= 117.4
)
const targetPoints = beijingPoints.length > 0 ? beijingPoints : points
if (targetPoints.length === 0) return
if (targetPoints.length === 1) {
map.setView(targetPoints[0], 13)
} else {
const bounds = L.latLngBounds(points)
const bounds = L.latLngBounds(targetPoints)
map.fitBounds(bounds, { padding: [30, 30] })
}
}, [points, map])
@@ -292,7 +292,7 @@ export function AdjustmentTab({ month }: { month: string }) {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" tick={{ fontSize: 9 }} />
<YAxis tick={{ fontSize: 10 }} />
<Tooltip />
<Tooltip formatter={(v: any) => [v, '日销量(账单数)']} />
<Legend />
<Line dataKey="bills" name="日销量(账单数)" stroke="#3b82f6" />
</LineChart>
@@ -77,7 +77,7 @@ export function BomTab({ month }: { month: string }) {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="band" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 10 }} />
<Tooltip />
<Tooltip formatter={(v: any) => [v, 'SKU数']} />
<Bar dataKey="cnt" name="SKU数" fill="#3b82f6" />
</BarChart>
</ResponsiveContainer>
@@ -5,7 +5,7 @@ import { FilterableTable } from '@/components/FilterableTable'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
import { useState } from 'react'
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid } from 'recharts'
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Legend } from 'recharts'
const PIE_COLORS = ['#22c55e', '#3b82f6', '#eab308', '#f97316', '#ef4444', '#8b5cf6', '#a3a3a3']
export function MaterialTab({ month }: { month: string }) {
@@ -119,7 +119,8 @@ export function MaterialTab({ month }: { month: string }) {
<Pie data={distData} dataKey="cnt" nameKey="loss_band" cx="50%" cy="50%" outerRadius={80} label={(e: any) => `${e.cnt}`}>
{distData.map((_: any, i: number) => <Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />)}
</Pie>
<Tooltip />
<Tooltip formatter={(v: any) => [v, '明细数']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
</PieChart>
</ResponsiveContainer>
<div className="flex-1">
@@ -5,7 +5,7 @@ import { MetricCard } from '@/components/MetricCard'
import { FilterableTable } from '@/components/FilterableTable'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { formatCurrency, formatPercent } from '@/lib/utils'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts'
const PIE_COLORS = ['#ef4444', '#f97316', '#eab308', '#3b82f6', '#22c55e', '#a3a3a3', '#8b5cf6']
@@ -100,7 +100,8 @@ export function OverviewTab({ month }: { month: string }) {
<Pie data={devs} dataKey="cnt" nameKey="deviation_band" cx="50%" cy="50%" outerRadius={80} label={(e: any) => `${e.deviation_band}: ${e.cnt}`}>
{devs.map((_: any, i: number) => <Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />)}
</Pie>
<Tooltip />
<Tooltip formatter={(v: any) => [v, '菜品数']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
</PieChart>
</ResponsiveContainer>
<div className="flex-1">
@@ -6,7 +6,7 @@ import { FilterableTable } from '@/components/FilterableTable'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { formatCurrency, formatPercent, formatNumber } from '@/lib/utils'
import { useState } from 'react'
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer, Legend } from 'recharts'
const PAGE_SIZE = 15
const LEVEL_COLORS: Record<string, string> = {
@@ -62,7 +62,8 @@ export function StoreTab({ month }: { month: string }) {
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80} label={(e: any) => `${e.name}: ${e.value}`}>
{pieData.map((d, i) => <Cell key={i} fill={LEVEL_COLORS[d.name]} />)}
</Pie>
<Tooltip />
<Tooltip formatter={(v: any) => [v, '门店数']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
</PieChart>
</ResponsiveContainer>
</CollapsibleSection>
@@ -32,10 +32,10 @@ export function ExpenseOverviewTab({ month }: { month: string }) {
<CollapsibleSection title="费用结构分析" subtitle="2026年4月各费用科目占比">
<ResponsiveContainer width="100%" height={400}>
<PieChart>
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={120} label={(e: any) => e.name.length > 6 ? e.name.slice(0, 6) + '…' : e.name}>
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={120} labelLine={false}>
{pieData.map((_: any, i: number) => <Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />)}
</Pie>
<Tooltip formatter={(v: any) => formatCurrency(v)} />
<Tooltip formatter={(v: any, name: any) => [formatCurrency(v), name]} />
<Legend wrapperStyle={{ fontSize: 11 }} />
</PieChart>
</ResponsiveContainer>
+2 -2
View File
@@ -371,7 +371,7 @@ export function BankPage() {
<div>
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie data={riskPie} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80} label={(e: any) => `${e.name}: ${e.value}`}>
<Pie data={riskPie} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={70} labelLine={false} label={(e: any) => <tspan fontSize={11}>{`${e.name}: ${e.value}`}</tspan>}>
{riskPie.map((entry: any, i: number) => <Cell key={i} fill={entry.fill} />)}
</Pie>
<Tooltip formatter={(v: any, name: any) => [`${v}`, name]} />
@@ -405,7 +405,7 @@ export function BankPage() {
<div className="mt-4">
<ResponsiveContainer width="100%" height={200}>
<PieChart>
<Pie data={channelPie} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={70} label={(e: any) => `${CHANNEL_LABELS[e.name] || e.name} ${totalChannel > 0 ? (e.value / totalChannel * 100).toFixed(1) : 0}%`}>
<Pie data={channelPie} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={60} labelLine={false} label={(e: any) => <tspan fontSize={11}>{`${CHANNEL_LABELS[e.name] || e.name} ${totalChannel > 0 ? (e.value / totalChannel * 100).toFixed(1) : 0}%`}</tspan>}>
{channelPie.map((entry: any, i: number) => <Cell key={i} fill={CHANNEL_COLORS[entry.name] || '#999'} />)}
</Pie>
<Tooltip formatter={(v: any, name: any) => [formatCurrency(v), CHANNEL_LABELS[name] || name]} />
+1 -1
View File
@@ -417,7 +417,7 @@ export function BossPage() {
{!structLoading && pieData.length > 0 && (
<ResponsiveContainer width="100%" height={280}>
<PieChart>
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={100} label={(e: any) => `${e.name}`}>
<Pie data={pieData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80} labelLine={false} label={(e: any) => <tspan fontSize={11}>{e.name}</tspan>}>
{pieData.map((_: any, i: number) => <Cell key={i} fill={PIE_COLORS[i % PIE_COLORS.length]} />)}
</Pie>
<Tooltip formatter={(v: any) => [formatCurrency(v), '金额']} />
+6 -6
View File
@@ -376,15 +376,15 @@ export function DashboardPage() {
<PieChart>
<Pie
data={riskPieData}
cx="50%" cy="50%" outerRadius={80}
cx="50%" cy="50%" outerRadius={70}
dataKey="value"
label={({ name, value, pct }: any) => `${name} ${value}家 (${pct}%)`}
labelLine={false} label={({ name, value, pct }: any) => <tspan fontSize={11}>{`${name} ${value}家 (${pct}%)`}</tspan>}
>
{riskPieData.map((entry) => (
<Cell key={entry.name} fill={RISK_COLORS[entry.name as keyof typeof RISK_COLORS] || '#gray'} />
))}
</Pie>
<Tooltip />
<Tooltip formatter={(v: any) => [`${v}`, '门店数']} />
<Legend />
</PieChart>
</ResponsiveContainer>
@@ -417,8 +417,8 @@ export function DashboardPage() {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" tick={{ fontSize: 10 }} />
<YAxis tickFormatter={(v) => v >= 10000 ? `${(v / 10000).toFixed(0)}` : v} tick={{ fontSize: 10 }} />
<Tooltip formatter={(v: any) => formatCurrency(v)} />
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
<Tooltip formatter={(v: any) => [formatCurrency(v), '金额']} />
<Bar dataKey="value" name="金额" radius={[4, 4, 0, 0]}>
{waterfallData.map((entry, idx) => <Cell key={idx} fill={entry.fill} />)}
</Bar>
</BarChart>
@@ -432,7 +432,7 @@ export function DashboardPage() {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" tick={{ fontSize: 9 }} angle={-30} textAnchor="middle" height={50} />
<YAxis unit="%" tick={{ fontSize: 10 }} />
<Tooltip />
<Tooltip formatter={(v: any) => [`${v}%`, '成本率']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
<Bar dataKey="meituan" name="美团" fill="#f97316" />
<Bar dataKey="eleme" name="饿了么" fill="#3b82f6" />
@@ -104,8 +104,8 @@ export function DistributionReconciliationPage() {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" tick={{ fontSize: 11 }} />
<YAxis tickFormatter={(v) => `${(v / 10000).toFixed(0)}`} tick={{ fontSize: 11 }} />
<Tooltip formatter={(v: any) => formatCurrency(v)} />
<Bar dataKey="value" radius={[4, 4, 0, 0]}>
<Tooltip formatter={(v: any) => [formatCurrency(v), '金额']} />
<Bar dataKey="value" name="金额" radius={[4, 4, 0, 0]}>
{waterfallData.map((entry, idx) => (
<Cell key={idx} fill={
entry.type === 'add' ? '#52c41a' :
+3 -3
View File
@@ -101,11 +101,11 @@ export function MemberPage() {
<CollapsibleSection title="会员账单占比" subtitle="会员与非会员账单分布">
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie data={pieData} cx="50%" cy="50%" outerRadius={80} dataKey="value"
label={({ name, value, }: any) => `${name}: ${formatNumber(value)}`}>
<Pie data={pieData} cx="50%" cy="50%" outerRadius={70} dataKey="value"
labelLine={false} label={({ name, value }: any) => <tspan fontSize={11}>{`${name}: ${formatNumber(value)}`}</tspan>}>
{pieData.map((_, i) => <Cell key={i} fill={PIE_COLORS[i]} />)}
</Pie>
<Tooltip formatter={(v: any) => formatNumber(v)} />
<Tooltip formatter={(v: any) => [formatNumber(v), '数量']} />
<Legend />
</PieChart>
</ResponsiveContainer>
+4 -3
View File
@@ -108,11 +108,12 @@ export function MonthlyReviewPage() {
{pieData.length > 0 ? (
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie data={pieData} cx="50%" cy="50%" outerRadius={80} dataKey="value"
label={({ name, value }: any) => `${name}: ${value}`}>
<Pie data={pieData} cx="50%" cy="50%" outerRadius={70} dataKey="value"
labelLine={false} label={({ name, value }: any) => <tspan fontSize={11}>{`${name}: ${value}`}</tspan>}>
{pieData.map((d) => <Cell key={d.name} fill={REVIEW_COLORS[d.name as keyof typeof REVIEW_COLORS]} />)}
</Pie>
<Tooltip />
<Tooltip formatter={(v: any) => [v, '任务数']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
</PieChart>
</ResponsiveContainer>
) : <div className="py-20 text-center text-muted-foreground"></div>}
+21 -7
View File
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, PieChart, Pie } from 'recharts'
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, PieChart, Pie, Legend } from 'recharts'
import api from '@/lib/api'
import { MetricCard } from '@/components/MetricCard'
import { LoadingSpinner } from '@/components/LoadingSpinner'
@@ -100,12 +100,13 @@ export function ProductionPlanPage() {
<CollapsibleSection title="BOM覆盖率" subtitle="有BOM的菜品占比">
<ResponsiveContainer width="100%" height={250}>
<PieChart>
<Pie data={coverageData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80} label={(entry: any) => `${entry.name}: ${entry.value}`}>
<Pie data={coverageData} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={70} labelLine={false} label={(entry: any) => <tspan fontSize={11}>{`${entry.name}: ${entry.value}`}</tspan>}>
{coverageData.map((entry, idx) => (
<Cell key={idx} fill={entry.fill} />
))}
</Pie>
<Tooltip />
<Tooltip formatter={(v: any) => [v, '菜品数']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
</PieChart>
</ResponsiveContainer>
</CollapsibleSection>
@@ -164,10 +165,17 @@ export function ProductionPlanPage() {
</CollapsibleSection>
{/* 生产与配送协同 */}
<CollapsibleSection title="生产与配送协同" subtitle="完工入库 → 配送 → 门店消耗 → 期末库存">
<CollapsibleSection title="生产与配送协同" subtitle="完工入库 → 中央厨房出库(配送) → 门店消耗 → 期末库存">
<div className="mb-3 flex items-center gap-2">
<Truck className="h-4 w-4 text-blue-600" />
<span className="text-sm text-muted-foreground"> = / = / = /</span>
<span className="text-sm text-muted-foreground">
() () /
</span>
</div>
<div className="mb-3 rounded-lg border bg-blue-50/40 p-3 text-xs text-blue-800">
<strong></strong>() () () <br />
<strong></strong>DC=2 <strong></strong><br />
<strong></strong> = / <strong></strong> = / <strong></strong> = /
</div>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={coordChartData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
@@ -188,8 +196,10 @@ export function ProductionPlanPage() {
sortOptions={[
{ key: 'inbound_qty', label: '完工入库' },
{ key: 'dist_qty', label: '配送量' },
{ key: 'ck_consumption_qty', label: '中央厨房出库' },
{ key: 'other_wh_consumption_qty', label: '其他仓库消耗' },
{ key: 'consumption_qty', label: '门店消耗' },
{ key: 'ending_qty', label: '期末库存' },
{ key: 'ending_qty', label: '门店期末库存' },
{ key: 'actual_cost', label: '实际成本' },
]}
defaultSort="actual_cost"
@@ -198,8 +208,12 @@ export function ProductionPlanPage() {
{ key: 'product_name', label: '产品名称' },
{ key: 'inbound_qty', label: '完工入库', align: 'right', render: (r: any) => formatNumber(r.inbound_qty) },
{ key: 'dist_qty', label: '配送量', align: 'right', render: (r: any) => formatNumber(r.dist_qty) },
{ key: 'ck_consumption_qty', label: '中央厨房出库', align: 'right', render: (r: any) => formatNumber(r.ck_consumption_qty) },
{ key: 'ck_ending_qty', label: '中央厨房期末', align: 'right', render: (r: any) => formatNumber(r.ck_ending_qty) },
{ key: 'other_wh_consumption_qty', label: '其他仓库消耗', align: 'right', render: (r: any) => formatNumber(r.other_wh_consumption_qty) },
{ key: 'other_wh_ending_qty', label: '其他仓库期末', align: 'right', render: (r: any) => formatNumber(r.other_wh_ending_qty) },
{ key: 'consumption_qty', label: '门店消耗', align: 'right', render: (r: any) => formatNumber(r.consumption_qty) },
{ key: 'ending_qty', label: '期末库存', align: 'right', render: (r: any) => formatNumber(r.ending_qty) },
{ key: 'ending_qty', label: '门店期末库存', align: 'right', render: (r: any) => formatNumber(r.ending_qty) },
{ key: 'completion_distribution_rate', label: '完工配送率', align: 'right', render: (r: any) => (
<span className={rateColorClass(r.completion_distribution_rate)}>{rateText(r.completion_distribution_rate)}</span>
) },
+2 -2
View File
@@ -194,7 +194,7 @@ export function RevenuePage() {
{channelPie.length > 0 && (
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie data={channelPie} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={100} label={(e: any) => `${channelLabels[e.name] || e.name}`}>
<Pie data={channelPie} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80} labelLine={false} label={(e: any) => <tspan fontSize={11}>{channelLabels[e.name] || e.name}</tspan>}>
{channelPie.map((entry: any, i: number) => <Cell key={i} fill={CHANNEL_COLORS[entry.name] || '#999'} />)}
</Pie>
<Tooltip formatter={(v: any, name: any) => [formatCurrency(v), channelLabels[name] || name]} />
@@ -225,7 +225,7 @@ export function RevenuePage() {
<div className="grid gap-4 lg:grid-cols-2">
<ResponsiveContainer width="100%" height={280}>
<PieChart>
<Pie data={mealPie} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={100} label={(e: any) => `${e.name}`}>
<Pie data={mealPie} dataKey="value" nameKey="name" cx="50%" cy="50%" outerRadius={80} labelLine={false} label={(e: any) => <tspan fontSize={11}>{e.name}</tspan>}>
{mealPie.map((entry: any, i: number) => <Cell key={i} fill={entry.fill} />)}
</Pie>
<Tooltip formatter={(v: any, name: any) => [formatCurrency(v), name]} />
+130 -13
View File
@@ -46,8 +46,16 @@ export function SKUPage() {
const totalSku = (skuData as any)?.data?.length || 0
const totalReceived = Object.values(abcSummary).reduce((s: number, v: any) => s + v.received, 0)
const cClassCount = abcSummary['C-长尾']?.sku_count || 0
const lowValueSku = ((skuData as any)?.data || []).filter((r: any) => Number(r.received_amount) < 1000).length
const singleStoreSku = ((skuData as any)?.data || []).filter((r: any) => Number(r.store_count) <= 1).length
const lowValueSkuRows = ((skuData as any)?.data || []).filter((r: any) => Number(r.received_amount) < 1000).sort((a: any, b: any) => Number(a.received_amount) - Number(b.received_amount))
const singleStoreSkuRows = ((skuData as any)?.data || []).filter((r: any) => Number(r.store_count) <= 1).sort((a: any, b: any) => Number(a.received_amount) - Number(b.received_amount))
const mergeCandidateRows = ((skuData as any)?.data || []).filter((r: any) => {
const name = (r.dish_name || '').toLowerCase()
return name.includes('套餐') || name.includes('赠') || name.includes('餐具') || name.includes('打包') || name.includes('纸巾') || name.includes('调料包')
}).sort((a: any, b: any) => Number(a.received_amount) - Number(b.received_amount))
const lowValueSku = lowValueSkuRows.length
const singleStoreSku = singleStoreSkuRows.length
const [detailView, setDetailView] = useState<string | null>(null)
const pageLoading = skuLoading || catLoading
@@ -84,7 +92,7 @@ export function SKUPage() {
data={abcPieData}
cx="50%" cy="50%" outerRadius={80}
dataKey="value"
label={({ name, value, share }: any) => `${name}: ${value}个 (${Number(share).toFixed(1)}%)`}
labelLine={false}
>
{abcPieData.map((entry) => (
<Cell key={entry.name} fill={ABC_COLORS[entry.name as keyof typeof ABC_COLORS] || '#gray'} />
@@ -102,7 +110,7 @@ export function SKUPage() {
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" tick={{ fontSize: 10 }} tickFormatter={(v) => v >= 10000 ? `${(v / 10000).toFixed(0)}` : v} />
<YAxis type="category" dataKey="category_name" tick={{ fontSize: 10 }} width={80} />
<Tooltip formatter={(v: any) => formatCurrency(v)} />
<Tooltip formatter={(v: any) => [formatCurrency(v), '实收']} />
<Bar dataKey="amount" fill="#3b82f6" name="实收" />
</BarChart>
</ResponsiveContainer>
@@ -147,18 +155,127 @@ export function SKUPage() {
{/* 长尾治理建议 */}
<CollapsibleSection title="长尾治理建议" subtitle="基于SKU分布的治理方向" defaultOpen={false}>
<div className="space-y-3 text-sm">
<div className="rounded-md border border-red-200 bg-red-50/50 p-3">
<p className="font-medium text-red-700"> {lowValueSku} SKU</p>
<p className="mt-1 text-xs text-muted-foreground">¥1,000SKU</p>
<div
className={`rounded-md border p-3 cursor-pointer transition-colors ${detailView === 'lowValue' ? 'border-red-400 bg-red-50' : 'border-red-200 bg-red-50/50 hover:bg-red-50'}`}
onClick={() => setDetailView(detailView === 'lowValue' ? null : 'lowValue')}
>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-red-700"> {lowValueSku} SKU</p>
<p className="mt-1 text-xs text-muted-foreground">¥1,000SKU</p>
</div>
<span className="text-xs text-red-600">{detailView === 'lowValue' ? '收起' : '查看明细 →'}</span>
</div>
</div>
<div className="rounded-md border border-yellow-200 bg-yellow-50/50 p-3">
<p className="font-medium text-yellow-700">SKU{singleStoreSku} </p>
<p className="mt-1 text-xs text-muted-foreground">1广</p>
{detailView === 'lowValue' && (
<FilterableTable
data={lowValueSkuRows}
sortOptions={[
{ key: 'received_amount', label: '实收' },
{ key: 'bill_count', label: '账单数' },
{ key: 'store_count', label: '覆盖门店' },
]}
defaultSort="received_amount"
defaultOrder="asc"
filterKey="category_level1"
filterLabel="全部品类"
columns={[
{ key: 'dish_name', label: '菜品名称' },
{ key: 'category_level1', label: '一级品类' },
{ key: 'abc_class', label: 'ABC', render: (r) => (
<span className={`rounded px-2 py-0.5 text-xs font-medium ${
r.abc_class === 'A-核心' ? 'bg-green-100 text-green-700' :
r.abc_class === 'B-成长' ? 'bg-blue-100 text-blue-700' :
'bg-red-100 text-red-700'
}`}>{r.abc_class}</span>
)},
{ key: 'store_count', label: '覆盖门店', align: 'center' },
{ key: 'bill_count', label: '账单数', align: 'right', render: (r) => formatNumber(r.bill_count) },
{ key: 'received_amount', label: '实收', align: 'right', render: (r) => formatCurrency(r.received_amount) },
]}
/>
)}
<div
className={`rounded-md border p-3 cursor-pointer transition-colors ${detailView === 'singleStore' ? 'border-yellow-400 bg-yellow-50' : 'border-yellow-200 bg-yellow-50/50 hover:bg-yellow-50'}`}
onClick={() => setDetailView(detailView === 'singleStore' ? null : 'singleStore')}
>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-yellow-700">SKU{singleStoreSku} </p>
<p className="mt-1 text-xs text-muted-foreground">1广</p>
</div>
<span className="text-xs text-yellow-600">{detailView === 'singleStore' ? '收起' : '查看明细 →'}</span>
</div>
</div>
<div className="rounded-md border border-blue-200 bg-blue-50/50 p-3">
<p className="font-medium text-blue-700">/300//</p>
<p className="mt-1 text-xs text-muted-foreground">SKU或合并为统一编码SKU池800-900</p>
{detailView === 'singleStore' && (
<FilterableTable
data={singleStoreSkuRows}
sortOptions={[
{ key: 'received_amount', label: '实收' },
{ key: 'bill_count', label: '账单数' },
]}
defaultSort="received_amount"
defaultOrder="asc"
filterKey="category_level1"
filterLabel="全部品类"
columns={[
{ key: 'dish_name', label: '菜品名称' },
{ key: 'category_level1', label: '一级品类' },
{ key: 'abc_class', label: 'ABC', render: (r) => (
<span className={`rounded px-2 py-0.5 text-xs font-medium ${
r.abc_class === 'A-核心' ? 'bg-green-100 text-green-700' :
r.abc_class === 'B-成长' ? 'bg-blue-100 text-blue-700' :
'bg-red-100 text-red-700'
}`}>{r.abc_class}</span>
)},
{ key: 'store_count', label: '覆盖门店', align: 'center' },
{ key: 'bill_count', label: '账单数', align: 'right', render: (r) => formatNumber(r.bill_count) },
{ key: 'received_amount', label: '实收', align: 'right', render: (r) => formatCurrency(r.received_amount) },
]}
/>
)}
<div
className={`rounded-md border p-3 cursor-pointer transition-colors ${detailView === 'merge' ? 'border-blue-400 bg-blue-50' : 'border-blue-200 bg-blue-50/50 hover:bg-blue-50'}`}
onClick={() => setDetailView(detailView === 'merge' ? null : 'merge')}
>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-blue-700">/{mergeCandidateRows.length} //</p>
<p className="mt-1 text-xs text-muted-foreground">SKU或合并为统一编码SKU池800-900</p>
</div>
<span className="text-xs text-blue-600">{detailView === 'merge' ? '收起' : '查看明细 →'}</span>
</div>
</div>
{detailView === 'merge' && (
<FilterableTable
data={mergeCandidateRows}
sortOptions={[
{ key: 'received_amount', label: '实收' },
{ key: 'bill_count', label: '账单数' },
{ key: 'store_count', label: '覆盖门店' },
]}
defaultSort="received_amount"
defaultOrder="asc"
filterKey="category_level1"
filterLabel="全部品类"
columns={[
{ key: 'dish_name', label: '菜品名称' },
{ key: 'category_level1', label: '一级品类' },
{ key: 'abc_class', label: 'ABC', render: (r) => (
<span className={`rounded px-2 py-0.5 text-xs font-medium ${
r.abc_class === 'A-核心' ? 'bg-green-100 text-green-700' :
r.abc_class === 'B-成长' ? 'bg-blue-100 text-blue-700' :
'bg-red-100 text-red-700'
}`}>{r.abc_class}</span>
)},
{ key: 'store_count', label: '覆盖门店', align: 'center' },
{ key: 'bill_count', label: '账单数', align: 'right', render: (r) => formatNumber(r.bill_count) },
{ key: 'received_amount', label: '实收', align: 'right', render: (r) => formatCurrency(r.received_amount) },
]}
/>
)}
</div>
</CollapsibleSection>
</div>
@@ -151,7 +151,7 @@ function HealthScoreTab({ navigate, month }: { navigate: (path: string) => void;
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="label" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 10 }} />
<Tooltip />
<Tooltip formatter={(v: any) => [v, '门店数']} />
<Bar dataKey="count" name="门店数" radius={[4, 4, 0, 0]}>
{scoreBuckets.map((b, i) => <Cell key={i} fill={b.color} />)}
</Bar>
@@ -256,8 +256,8 @@ function HealthScoreDetail({ store, onClose, navigate }: { store: any; onClose:
<PolarGrid />
<PolarAngleAxis dataKey="dimension" tick={{ fontSize: 10 }} />
<PolarRadiusAxis angle={90} domain={[0, 100]} tick={{ fontSize: 8 }} />
<Radar dataKey="score" stroke={healthColor} fill={healthColor} fillOpacity={0.3} />
<Tooltip />
<Radar dataKey="score" name="得分" stroke={healthColor} fill={healthColor} fillOpacity={0.3} />
<Tooltip formatter={(v: any) => [v, '得分']} />
</RadarChart>
</ResponsiveContainer>
</div>
+6 -5
View File
@@ -272,8 +272,8 @@ export function StoreDetailPage() {
<PolarGrid />
<PolarAngleAxis dataKey="dimension" tick={{ fontSize: 10 }} />
<PolarRadiusAxis angle={90} domain={[0, 100]} tick={{ fontSize: 8 }} />
<Radar dataKey="score" stroke={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fill={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fillOpacity={0.3} />
<Tooltip />
<Radar dataKey="score" name="得分" stroke={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fill={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fillOpacity={0.3} />
<Tooltip formatter={(v: any) => [v, '得分']} />
</RadarChart>
</ResponsiveContainer>
<div className="space-y-2">
@@ -393,10 +393,11 @@ export function StoreDetailPage() {
<p className="mb-2 text-xs font-medium text-muted-foreground"></p>
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie data={pieData} cx="50%" cy="50%" outerRadius={70} dataKey="value" label={({ name, value }: any) => `${name} ${value}%`}>
<Pie data={pieData} cx="50%" cy="50%" outerRadius={60} dataKey="value" nameKey="name" labelLine={false} label={({ name, value }: any) => <tspan fontSize={11}>{`${name} ${value}%`}</tspan>}>
{pieData.map((d, i) => <Cell key={i} fill={d.fill} />)}
</Pie>
<Tooltip />
<Tooltip formatter={(v: any) => [`${v}%`, '占比']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
</PieChart>
</ResponsiveContainer>
</div>
@@ -407,7 +408,7 @@ export function StoreDetailPage() {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" tick={{ fontSize: 10 }} />
<YAxis unit="%" tick={{ fontSize: 10 }} />
<Tooltip />
<Tooltip formatter={(v: any) => [`${v}%`, '占比']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
<Bar dataKey="门店占比" fill="#3b82f6" />
<Bar dataKey="公司占比" fill="#e2e8f0" />
+17 -16
View File
@@ -14,7 +14,7 @@ const SCORE_DIMENSIONS = [
{ key: 'score_revenue', label: '营收规模', max: 20 },
{ key: 'score_cost', label: '成本控制', max: 20 },
{ key: 'score_margin', label: '毛利率', max: 15 },
{ key: 'score_risk', label: '风险等级', max: 10 },
{ key: 'score_risk', label: '风险控制', max: 10 },
{ key: 'score_repeat', label: '复购率', max: 10 },
{ key: 'score_member', label: '会员占比', max: 10 },
{ key: 'score_task', label: '任务完成', max: 10 },
@@ -46,7 +46,7 @@ export function StorePage() {
})
const stores = ((storesData as any)?.data || []).map((s: any) => ({ code: s.store_code, name: s.store_name, risk: s.risk_level, issue: s.primary_issue }))
const { data: priorityData } = useQuery({
const { data: priorityData, isLoading: priorityLoading } = useQuery({
queryKey: ['stores-priority-detail', month],
queryFn: () => api.get('/stores/priority', { params: { month } }),
})
@@ -71,23 +71,23 @@ export function StorePage() {
queryFn: () => api.get('/tasks/followup', { params: { month } }),
})
const { data: skuData } = useQuery({
const { data: skuData, isLoading: skuLoading } = useQuery({
queryKey: ['sku-attach', storeCode],
queryFn: () => api.get('/sku/attach', { params: { month } }),
})
const { data: abcData } = useQuery({
const { data: abcData, isLoading: abcLoading } = useQuery({
queryKey: ['sku-abc'],
queryFn: () => api.get('/sku/abc', { params: { month } }),
})
// StoreDetailPage queries
const { data: storeDetailData } = useQuery({
const { data: storeDetailData, isLoading: storeDetailLoading } = useQuery({
queryKey: ['store-detail', storeCode, month],
queryFn: () => api.get(`/stores/${storeCode}`, { params: { month } }),
})
const { data: healthData } = useQuery({
const { data: healthData, isLoading: healthLoading } = useQuery({
queryKey: ['store-health', storeCode, month],
queryFn: () => api.get('/situational-awareness/health-score', { params: { month } }),
})
@@ -217,7 +217,7 @@ export function StorePage() {
return 'bg-green-100 text-green-700 border-green-300'
}
const pageLoading = storesLoading || cardLoading || tasksLoading || dailyLoading || followupLoading
const pageLoading = storesLoading || priorityLoading || cardLoading || tasksLoading || dailyLoading || followupLoading || skuLoading || abcLoading || storeDetailLoading || healthLoading
if (pageLoading) {
return <LoadingSpinner text="加载店长工作台..." />
@@ -267,7 +267,7 @@ export function StorePage() {
{/* 经营概览 */}
<div className="rounded-lg border bg-card p-4">
<div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-bold">{month}</h2>
<h2 className="text-sm font-bold">{month}</h2>
{anomalyCount > 0 && (
<span className="rounded-md bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">{anomalyCount} </span>
)}
@@ -510,10 +510,10 @@ export function StorePage() {
{/* ========== 门店详情区 ========== */}
{sc && Number(sc.received) > 0 && (
<>
{/* 核心指标 */}
{/* 核心指标 - 月度汇总 */}
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
<MetricCard title="实收" value={sc.received} format="currency" />
<MetricCard title="账单数" value={sc.bill_count} format="number" />
<MetricCard title="月度实收" value={sc.received} format="currency" />
<MetricCard title="月度账单数" value={sc.bill_count} format="number" />
<MetricCard title="客单价" value={sc.avg_bill_value} format="currency" />
<MetricCard title="优惠率" value={sc.discount_rate_pct} format="percent" />
<MetricCard title="理论毛利率" value={sc.theoretical_margin_pct} format="percent" />
@@ -611,8 +611,8 @@ export function StorePage() {
<PolarGrid />
<PolarAngleAxis dataKey="dimension" tick={{ fontSize: 10 }} />
<PolarRadiusAxis angle={90} domain={[0, 100]} tick={{ fontSize: 8 }} />
<Radar dataKey="score" stroke={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fill={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fillOpacity={0.3} />
<Tooltip />
<Radar dataKey="score" name="得分" stroke={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fill={Number(health.health_score) >= 75 ? '#22c55e' : Number(health.health_score) >= 55 ? '#eab308' : '#ef4444'} fillOpacity={0.3} />
<Tooltip formatter={(v: any) => [v, '得分']} />
</RadarChart>
</ResponsiveContainer>
<div className="space-y-2">
@@ -718,10 +718,11 @@ export function StorePage() {
<p className="mb-2 text-xs font-medium text-muted-foreground"></p>
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie data={pieData} cx="50%" cy="50%" outerRadius={70} dataKey="value" label={({ name, value }: any) => `${name} ${value}%`}>
<Pie data={pieData} cx="50%" cy="50%" outerRadius={60} dataKey="value" nameKey="name" labelLine={false} label={({ name, value }: any) => <tspan fontSize={11}>{`${name} ${value}%`}</tspan>}>
{pieData.map((d, i) => <Cell key={i} fill={d.fill} />)}
</Pie>
<Tooltip />
<Tooltip formatter={(v: any) => [`${v}%`, '占比']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
</PieChart>
</ResponsiveContainer>
</div>
@@ -732,7 +733,7 @@ export function StorePage() {
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" tick={{ fontSize: 10 }} />
<YAxis unit="%" tick={{ fontSize: 10 }} />
<Tooltip />
<Tooltip formatter={(v: any) => [`${v}%`, '占比']} />
<Legend wrapperStyle={{ fontSize: 10 }} />
<Bar dataKey="门店占比" fill="#3b82f6" />
<Bar dataKey="公司占比" fill="#e2e8f0" />
+69 -26
View File
@@ -1597,6 +1597,13 @@ router.get('/central-kitchen/dashboard', async (req: AuthRequest, res) => {
router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
try {
const month = parseMonth(req)
// 检查配送物化视图是否包含当前月份数据
const distMvCheck = await query(`SELECT 1 FROM mv_distribution_monthly WHERE report_month = $1::date LIMIT 1`, [month])
if (distMvCheck.rows.length === 0) {
await query(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_distribution_monthly`)
}
const [summary, storeReconciliation, topVariances, unmatchedItems, categoryReconciliation] = await Promise.all([
// 汇总
query(`
@@ -1605,9 +1612,8 @@ router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
round(sum(d.total_quantity)::numeric,2) AS dist_qty,
round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt,
round(sum(d.cost_excl_tax_amount)::numeric,2) AS dist_cost_excl_tax
FROM distribution_detail_records d
WHERE d.business_date >= $1::date AND d.business_date < ($1::date + INTERVAL '1 month')
AND NOT d.is_return AND d.item_code IS NOT NULL
FROM mv_distribution_monthly d
WHERE d.report_month = $1::date
GROUP BY d.store_code, d.item_code
),
inv AS (
@@ -1658,9 +1664,8 @@ router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
round(sum(d.total_quantity)::numeric,2) AS dist_qty,
round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt,
round(sum(d.cost_excl_tax_amount)::numeric,2) AS dist_cost_excl_tax
FROM distribution_detail_records d
WHERE d.business_date >= $1::date AND d.business_date < ($1::date + INTERVAL '1 month')
AND NOT d.is_return AND d.item_code IS NOT NULL
FROM mv_distribution_monthly d
WHERE d.report_month = $1::date
GROUP BY d.store_code
),
inv AS (
@@ -1674,8 +1679,8 @@ router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
GROUP BY f.store_code
),
store_names AS (
SELECT DISTINCT store_code, store_name FROM distribution_detail_records
WHERE business_date >= $1::date AND business_date < ($1::date + INTERVAL '1 month')
SELECT DISTINCT store_code, store_name FROM mv_distribution_monthly
WHERE report_month = $1::date
)
SELECT COALESCE(d.store_code, i.store_code) AS store_code,
sn.store_name,
@@ -1699,9 +1704,8 @@ router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
SELECT d.store_code, d.item_code, d.item_name, d.unit, d.major_category, d.minor_category,
round(sum(d.total_quantity)::numeric,2) AS dist_qty,
round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt
FROM distribution_detail_records d
WHERE d.business_date >= $1::date AND d.business_date < ($1::date + INTERVAL '1 month')
AND NOT d.is_return AND d.item_code IS NOT NULL
FROM mv_distribution_monthly d
WHERE d.report_month = $1::date
GROUP BY d.store_code, d.item_code, d.item_name, d.unit, d.major_category, d.minor_category
),
inv AS (
@@ -1733,9 +1737,8 @@ router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
round(sum(d.total_quantity)::numeric,2) AS dist_qty,
round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt,
count(DISTINCT d.store_code) AS store_count
FROM distribution_detail_records d
WHERE d.business_date >= $1::date AND d.business_date < ($1::date + INTERVAL '1 month')
AND NOT d.is_return AND d.item_code IS NOT NULL
FROM mv_distribution_monthly d
WHERE d.report_month = $1::date
GROUP BY d.item_code, d.item_name, d.unit, d.major_category, d.minor_category
),
inv_items AS (
@@ -1757,16 +1760,14 @@ router.get('/distribution/reconciliation', async (req: AuthRequest, res) => {
round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt,
round(sum(d.cost_excl_tax_amount)::numeric,2) AS dist_cost_excl_tax,
count(DISTINCT d.item_code) AS item_count
FROM distribution_detail_records d
WHERE d.business_date >= $1::date AND d.business_date < ($1::date + INTERVAL '1 month')
AND NOT d.is_return AND d.item_code IS NOT NULL
FROM mv_distribution_monthly d
WHERE d.report_month = $1::date
GROUP BY COALESCE(NULLIF(d.minor_category,''),'未分类')
),
item_cat AS (
SELECT DISTINCT item_code, COALESCE(NULLIF(minor_category,''),'未分类') AS minor_category
FROM distribution_detail_records
WHERE business_date >= $1::date AND business_date < ($1::date + INTERVAL '1 month')
AND item_code IS NOT NULL
FROM mv_distribution_monthly
WHERE report_month = $1::date
),
inv AS (
SELECT ic.minor_category,
@@ -2080,6 +2081,14 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
await client.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_dish_sales_monthly`)
}
// 检查配送物化视图是否包含当前月份数据
const distMvCheck = await client.query(`
SELECT 1 FROM mv_distribution_monthly WHERE report_month = $1::date LIMIT 1
`, [month])
if (distMvCheck.rows.length === 0) {
await client.query(`REFRESH MATERIALIZED VIEW CONCURRENTLY mv_distribution_monthly`)
}
// 从物化视图创建临时表(索引扫描,毫秒级)
await client.query(`
CREATE TEMP TABLE IF NOT EXISTS tmp_sales_agg AS
@@ -2159,9 +2168,8 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
LEFT JOIN analytics.dim_material m ON b.material_code = m.material_code
LEFT JOIN (
SELECT d.item_name, round(sum(d.outbound_total_amount)::numeric / NULLIF(sum(d.total_quantity), 0), 4) AS unit_price
FROM distribution_detail_records d
WHERE d.business_date >= $1::date AND d.business_date < ($1::date + INTERVAL '1 month')
AND NOT d.is_return AND d.item_name IS NOT NULL
FROM mv_distribution_monthly d
WHERE d.report_month = $1::date
GROUP BY d.item_name
) ic ON m.material_name = ic.item_name
GROUP BY s.store_code, b.material_code, m.material_name, b.unit
@@ -2279,12 +2287,33 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
SELECT d.item_code, d.item_name,
round(sum(d.total_quantity)::numeric,4) AS dist_qty,
round(sum(d.outbound_total_amount)::numeric,2) AS dist_amt
FROM distribution_detail_records d
WHERE d.business_date >= $1::date AND d.business_date < ($1::date + INTERVAL '1 month')
AND NOT d.is_return AND d.item_code IS NOT NULL
FROM mv_distribution_monthly d
WHERE d.report_month = $1::date
AND d.distribution_center_code = '2'
GROUP BY d.item_code, d.item_name
),
ck_inventory AS (
SELECT f.material_code,
round(sum(f.consumption_quantity)::numeric,4) AS consumption_qty,
round(sum(f.ending_quantity)::numeric,4) AS ending_qty
FROM analytics.fact_inventory_snapshot f
WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
AND f.store_code = '2'
GROUP BY f.material_code
),
other_wh_inventory AS (
SELECT f.material_code,
round(sum(f.consumption_quantity)::numeric,4) AS consumption_qty,
round(sum(f.ending_quantity)::numeric,4) AS ending_qty
FROM analytics.fact_inventory_snapshot f
WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
AND f.store_code IN (
SELECT cost_unit_code FROM inventory_store_mapping
WHERE sales_store_code IS NULL OR sales_store_code = ''
)
AND f.store_code != '2'
GROUP BY f.material_code
),
store_consumption AS (
SELECT f.material_code,
round(sum(f.consumption_quantity)::numeric,4) AS consumption_qty,
@@ -2293,6 +2322,10 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
round(sum(f.ending_amount)::numeric,2) AS ending_amt
FROM analytics.fact_inventory_snapshot f
WHERE f.snapshot_date >= $1::date AND f.snapshot_date < ($1::date + INTERVAL '1 month')
AND f.store_code NOT IN (
SELECT cost_unit_code FROM inventory_store_mapping
WHERE sales_store_code IS NULL OR sales_store_code = ''
)
GROUP BY f.material_code
)
SELECT COALESCE(p.product_code, dist.item_code) AS product_code,
@@ -2302,6 +2335,10 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
COALESCE(p.theoretical_cost, 0) AS theoretical_cost,
COALESCE(dist.dist_qty, 0) AS dist_qty,
COALESCE(dist.dist_amt, 0) AS dist_amt,
COALESCE(ck.consumption_qty, 0) AS ck_consumption_qty,
COALESCE(ck.ending_qty, 0) AS ck_ending_qty,
COALESCE(owh.consumption_qty, 0) AS other_wh_consumption_qty,
COALESCE(owh.ending_qty, 0) AS other_wh_ending_qty,
COALESCE(sc.consumption_qty, 0) AS consumption_qty,
COALESCE(sc.consumption_amt, 0) AS consumption_amt,
COALESCE(sc.ending_qty, 0) AS ending_qty,
@@ -2319,6 +2356,8 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
ELSE NULL END AS inventory_accumulation_rate
FROM ck_production p
FULL OUTER JOIN ck_distribution dist ON p.product_code = dist.item_code
FULL OUTER JOIN ck_inventory ck ON COALESCE(p.product_code, dist.item_code) = ck.material_code
FULL OUTER JOIN other_wh_inventory owh ON COALESCE(p.product_code, dist.item_code) = owh.material_code
FULL OUTER JOIN store_consumption sc ON COALESCE(p.product_code, dist.item_code) = sc.material_code
ORDER BY COALESCE(p.actual_cost, 0) DESC
LIMIT 50
@@ -2384,6 +2423,10 @@ router.get('/sales-driven/production-plan', async (req: AuthRequest, res) => {
theoretical_cost: parseNum(row.theoretical_cost),
dist_qty: parseNum(row.dist_qty),
dist_amt: parseNum(row.dist_amt),
ck_consumption_qty: parseNum(row.ck_consumption_qty),
ck_ending_qty: parseNum(row.ck_ending_qty),
other_wh_consumption_qty: parseNum(row.other_wh_consumption_qty),
other_wh_ending_qty: parseNum(row.other_wh_ending_qty),
consumption_qty: parseNum(row.consumption_qty),
consumption_amt: parseNum(row.consumption_amt),
ending_qty: parseNum(row.ending_qty),