feat: 全页面增加营业收入/优惠指标 + 修复趋势预测空数据 + 区域对比口径统一 + StorePage最新经营日概览
- 后端: 所有API的discount改为从bill_fact累计discount_total - 后端: revenue/daily-summary增加consumption字段 - 后端: situational-awareness/forecast修复c175字符串比较为timestamp - 后端: region/comparison去掉未知区域排除,统一口径 - 后端: daily-card增加营业收入/优惠指标,返回target_date/baseline_date - 前端: BossPage/DashboardPage/BankPage/StorePage/ExpenseOverviewTab/RegionalPage/RegionComparisonPage/RevenuePage 均增加营业收入和优惠指标卡 - 前端: StorePage日均经营概览改为最新经营日概览,显示具体对比日期 - 前端: 日期格式统一截取YYYY-MM-DD显示
This commit is contained in:
@@ -21,10 +21,12 @@ export function ExpenseOverviewTab({ month }: { month: string }) {
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<MetricCard title="经营门店" value={ov.total_stores} unit="家" description={`${ov.profitable_stores}家盈利 / ${ov.loss_stores}家亏损`} />
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
<MetricCard title="营业收入" value={ov.total_consumption} format="currency" description="全部门店消费金额合计" />
|
||||
<MetricCard title="优惠" value={ov.total_discount} format="currency" description="各类优惠扣减合计" />
|
||||
<MetricCard title="实收合计" value={ov.total_received} format="currency" description={`费用合计 ${formatCurrency(ov.total_expense)}`} />
|
||||
<MetricCard title="门店贡献利润" value={ov.total_contribution} format="currency" description={`贡献率 ${formatPercent(ov.overall_contribution_rate_pct)}`} />
|
||||
<MetricCard title="经营门店" value={ov.total_stores} unit="家" description={`${ov.profitable_stores}家盈利 / ${ov.loss_stores}家亏损`} />
|
||||
<MetricCard title="费用率" value={ov.overall_expense_rate_pct} format="percent" description={`人工 ${formatPercent(ov.overall_wage_rate_pct)} · 房租 ${formatPercent(ov.overall_rent_rate_pct)}`} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -137,11 +137,13 @@ export function BankPage() {
|
||||
const waterfallChart = useMemo(() => {
|
||||
if (!wf) return []
|
||||
return [
|
||||
{ name: '实收', value: wf.received, base: 0, fill: '#3b82f6' },
|
||||
{ name: '食材', value: -wf.food_cost, base: wf.received, fill: '#f97316' },
|
||||
{ name: '人工', value: -wf.wage, base: wf.received - wf.food_cost, fill: '#3b82f6' },
|
||||
{ name: '房租', value: -wf.rent, base: wf.received - wf.food_cost - wf.wage, fill: '#8b5cf6' },
|
||||
{ name: '水电', value: -wf.utility, base: wf.received - wf.food_cost - wf.wage - wf.rent, fill: '#06b6d4' },
|
||||
{ name: '营业收入', value: wf.consumption, base: 0, fill: '#3b82f6' },
|
||||
{ name: '优惠', value: -wf.discount, base: wf.consumption - wf.discount, fill: '#f97316' },
|
||||
{ name: '实收', value: wf.received, base: 0, fill: '#6366f1' },
|
||||
{ name: '食材', value: -wf.food_cost, base: wf.received - wf.food_cost, fill: '#f97316' },
|
||||
{ name: '人工', value: -wf.wage, base: wf.received - wf.food_cost - wf.wage, fill: '#3b82f6' },
|
||||
{ name: '房租', value: -wf.rent, base: wf.received - wf.food_cost - wf.wage - wf.rent, fill: '#8b5cf6' },
|
||||
{ name: '水电', value: -wf.utility, base: wf.received - wf.food_cost - wf.wage - wf.rent - wf.utility, fill: '#06b6d4' },
|
||||
{ name: '其他', value: -(wf.dorm + wf.commission + wf.other_expense), base: wf.received - wf.food_cost - wf.wage - wf.rent - wf.utility, fill: '#64748b' },
|
||||
{ name: '门店贡献利润', value: wf.store_contribution, base: 0, fill: wf.store_contribution >= 0 ? '#22c55e' : '#ef4444' },
|
||||
]
|
||||
@@ -312,11 +314,13 @@ export function BankPage() {
|
||||
|
||||
{/* ② 企业经营概况 */}
|
||||
<CollapsibleSection title="企业经营概况" subtitle="月度核心指标与日度营收趋势">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
<MetricCard title="营业收入" value={wf?.consumption} format="currency" description="全部门店消费金额合计" />
|
||||
<MetricCard title="优惠" value={wf?.discount} format="currency" description="各类优惠扣减合计" />
|
||||
<MetricCard title="月度实收" value={ov?.received} format="currency" description={`${formatNumber(ov?.bill_count)} 笔账单`} />
|
||||
<MetricCard title="客单价" value={ov?.avg_bill_value} format="currency" description="实收 ÷ 账单数" />
|
||||
<MetricCard title="理论毛利率" value={ov?.theoretical_margin_pct} format="percent" description="菜品定价毛利空间" />
|
||||
<MetricCard title="门店数量" value={stores.length} unit="家" description={`活跃经营门店总数`} />
|
||||
<MetricCard title="门店数量" value={stores.length} unit="家" description="活跃经营门店总数" />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
|
||||
@@ -154,7 +154,9 @@ export function BossPage() {
|
||||
|
||||
// 利润瀑布数据
|
||||
const waterfallSteps = wf ? [
|
||||
{ name: '实收', value: Number(wf.received), type: 'total' },
|
||||
{ name: '营业收入', value: Number(wf.consumption), type: 'total' },
|
||||
{ name: '优惠', value: -Number(wf.discount), type: 'cost' },
|
||||
{ name: '实收', value: Number(wf.received), type: 'subtotal' },
|
||||
{ name: '食材成本', value: -Number(wf.food_cost), type: 'cost' },
|
||||
{ name: '人工', value: -Number(wf.wage), type: 'cost' },
|
||||
{ name: '房租', value: -Number(wf.rent), type: 'cost' },
|
||||
@@ -171,6 +173,9 @@ export function BossPage() {
|
||||
if (step.type === 'total') {
|
||||
cumulative = step.value
|
||||
return { name: step.name, base: 0, value: step.value, fill: '#3b82f6' }
|
||||
} else if (step.type === 'subtotal') {
|
||||
cumulative = step.value
|
||||
return { name: step.name, base: 0, value: step.value, fill: '#6366f1' }
|
||||
} else if (step.type === 'profit') {
|
||||
return { name: step.name, base: 0, value: step.value, fill: '#22c55e' }
|
||||
} else {
|
||||
@@ -218,11 +223,13 @@ export function BossPage() {
|
||||
</div>
|
||||
|
||||
{/* ① 核心经营指标 */}
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<MetricCard title="实收总额" value={ex?.total_received} format="currency" trend={trendReceived} description={`全口径${ex?.total_stores}家门店。公式:全部门店实收合计。环比上周 ${trendReceived > 0 ? '↑' : '↓'} ${Math.abs(trendReceived)}%`} />
|
||||
<MetricCard title="门店贡献利润估算" value={ex?.actual_net_profit} format="currency" status={Number(ex?.actual_net_margin_pct) < 5 ? 'bad' : Number(ex?.actual_net_margin_pct) < 10 ? 'warn' : 'good'} description={`贡献率 ${formatPercent(ex?.actual_net_margin_pct)} · 公式:实收 - 食材成本 - 经营费用(仅费用已匹配门店)。建议值:≥ 10% 优秀,5-10% 合格,< 5% 需整改`} />
|
||||
<MetricCard title="门店数" value={ex?.total_stores} unit="家" description={`盈利 ${ex?.profitable_stores}家 / 亏损 ${ex?.loss_stores}家。无费用数据 ${ex?.no_expense_stores}家`} />
|
||||
<MetricCard title="客单价" value={ex?.avg_bill_value} format="currency" trend={trend((last7.length > 0 ? sumKey(last7, 'received') / sumKey(last7, 'bill_count') : 0), (prev7.length > 0 ? sumKey(prev7, 'received') / sumKey(prev7, 'bill_count') : 0))} description={`账单 ${formatNumber(ex?.total_bills)} 笔 · 公式:实收 ÷ 账单数`} />
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
<MetricCard title="营业收入" value={ex?.total_consumption} format="currency" description="全部门店消费金额合计" />
|
||||
<MetricCard title="优惠" value={ex?.total_discount} format="currency" description="各类优惠扣减合计" />
|
||||
<MetricCard title="实收总额" value={ex?.total_received} format="currency" trend={trendReceived} description={`全口径${ex?.total_stores}家门店。环比上周 ${trendReceived > 0 ? '↑' : '↓'} ${Math.abs(trendReceived)}%`} />
|
||||
<MetricCard title="门店贡献利润估算" value={ex?.actual_net_profit} format="currency" status={Number(ex?.actual_net_margin_pct) < 5 ? 'bad' : Number(ex?.actual_net_margin_pct) < 10 ? 'warn' : 'good'} description={`贡献率 ${formatPercent(ex?.actual_net_margin_pct)}`} />
|
||||
<MetricCard title="门店数" value={ex?.total_stores} unit="家" description={`盈利 ${ex?.profitable_stores}家 / 亏损 ${ex?.loss_stores}家`} />
|
||||
<MetricCard title="客单价" value={ex?.avg_bill_value} format="currency" trend={trend((last7.length > 0 ? sumKey(last7, 'received') / sumKey(last7, 'bill_count') : 0), (prev7.length > 0 ? sumKey(prev7, 'received') / sumKey(prev7, 'bill_count') : 0))} description={`账单 ${formatNumber(ex?.total_bills)} 笔`} />
|
||||
</div>
|
||||
|
||||
{/* ①b KPI达成率 */}
|
||||
|
||||
@@ -212,12 +212,14 @@ export function DashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* 经营指标 */}
|
||||
<CollapsibleSection title="经营指标" subtitle="实收、账单数、客单价等核心经营数据">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<MetricCard title="实收" value={od?.received} format="currency" trend={trendReceived} description="门店实际收到的金额,扣除优惠后的净收入" />
|
||||
<MetricCard title="账单数" value={od?.bill_count} format="number" trend={trendBills} description="全部门店累计的账单(订单)总数" />
|
||||
<MetricCard title="客单价" value={od?.avg_bill_value} format="currency" trend={trendAvgBill} description="平均每笔账单的实收金额 = 实收 ÷ 账单数" />
|
||||
<MetricCard title="P0+P1门店" value={p0p1Stores.length} format="number" description="需要重点整改的门店数:P0为紧急修复,P1为重点整改" />
|
||||
<CollapsibleSection title="经营指标" subtitle="营业收入、优惠、实收等核心经营数据">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
<MetricCard title="营业收入" value={od?.consumption} format="currency" description="全部门店消费金额合计" />
|
||||
<MetricCard title="优惠" value={od?.discount} format="currency" description="各类优惠扣减合计" />
|
||||
<MetricCard title="实收" value={od?.received} format="currency" trend={trendReceived} description="扣除优惠后的净收入" />
|
||||
<MetricCard title="账单数" value={od?.bill_count} format="number" trend={trendBills} description="全部门店累计的账单总数" />
|
||||
<MetricCard title="客单价" value={od?.avg_bill_value} format="currency" trend={trendAvgBill} description="实收 ÷ 账单数" />
|
||||
<MetricCard title="P0+P1门店" value={p0p1Stores.length} format="number" description="需重点整改的门店数" />
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ export function RegionComparisonPage() {
|
||||
const rows = (data as any)?.data || []
|
||||
const forecastRows = (forecastData as any)?.data || []
|
||||
|
||||
const totalConsumption = rows.reduce((s: number, r: any) => s + Number(r.total_consumption || 0), 0)
|
||||
const totalDiscount = rows.reduce((s: number, r: any) => s + Number(r.total_discount || 0), 0)
|
||||
const totalRevenue = rows.reduce((s: number, r: any) => s + Number(r.total_revenue || 0), 0)
|
||||
const totalStores = rows.reduce((s: number, r: any) => s + Number(r.store_count || 0), 0)
|
||||
const avgMargin = rows.length > 0 ? rows.reduce((s: number, r: any) => s + Number(r.avg_margin_pct || 0), 0) / rows.length : 0
|
||||
@@ -53,9 +55,11 @@ export function RegionComparisonPage() {
|
||||
|
||||
{/* 概览指标 */}
|
||||
<CollapsibleSection title="区域总览" subtitle="全区域汇总">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
<MetricCard title="营业收入" value={totalConsumption} format="currency" description="各区域消费金额合计" />
|
||||
<MetricCard title="优惠" value={totalDiscount} format="currency" description="各类优惠扣减合计" />
|
||||
<MetricCard title="实收" value={totalRevenue} format="currency" description="扣除优惠后净收入" />
|
||||
<MetricCard title="区域总数" value={rows.length} format="number" description="有数据的区域数量" />
|
||||
<MetricCard title="区域总收入" value={totalRevenue} format="currency" description="所有区域当月总收入" />
|
||||
<MetricCard title="门店总数" value={totalStores} format="number" description="所有区域门店数" />
|
||||
<MetricCard title="平均毛利率" value={avgMargin} format="percent" description="各区域平均理论毛利率" />
|
||||
</div>
|
||||
@@ -89,7 +93,9 @@ export function RegionComparisonPage() {
|
||||
filterKey="region"
|
||||
filterLabel="全部区域"
|
||||
sortOptions={[
|
||||
{ key: 'total_revenue', label: '总收入' },
|
||||
{ key: 'total_consumption', label: '营业收入' },
|
||||
{ key: 'total_discount', label: '优惠' },
|
||||
{ key: 'total_revenue', label: '实收' },
|
||||
{ key: 'revenue_per_store', label: '店均收入' },
|
||||
{ key: 'avg_bill_value', label: '客单价' },
|
||||
{ key: 'avg_margin_pct', label: '毛利率' },
|
||||
@@ -101,7 +107,9 @@ export function RegionComparisonPage() {
|
||||
columns={[
|
||||
{ key: 'region', label: '区域' },
|
||||
{ key: 'store_count', label: '门店数', align: 'right', render: (r) => formatNumber(r.store_count) },
|
||||
{ key: 'total_revenue', label: '总收入', align: 'right', render: (r) => formatCurrency(r.total_revenue) },
|
||||
{ key: 'total_consumption', label: '营业收入', align: 'right', render: (r) => formatCurrency(r.total_consumption) },
|
||||
{ key: 'total_discount', label: '优惠', align: 'right', render: (r) => formatCurrency(r.total_discount) },
|
||||
{ key: 'total_revenue', label: '实收', align: 'right', render: (r) => formatCurrency(r.total_revenue) },
|
||||
{ key: 'revenue_per_store', label: '店均收入', align: 'right', render: (r) => formatCurrency(r.revenue_per_store) },
|
||||
{ key: 'avg_bill_value', label: '客单价', align: 'right', render: (r) => formatCurrency(r.avg_bill_value) },
|
||||
{ key: 'bill_count', label: '账单数', align: 'right', render: (r) => formatNumber(r.bill_count) },
|
||||
|
||||
@@ -75,7 +75,10 @@ export function RegionalPage() {
|
||||
</div>
|
||||
|
||||
{/* 汇总卡片 */}
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-7">
|
||||
<MetricCard title="营业收入" value={regionRows.reduce((s: number, r: any) => s + Number(r.total_consumption || 0), 0)} format="currency" description="各区域消费金额合计" />
|
||||
<MetricCard title="优惠" value={regionRows.reduce((s: number, r: any) => s + Number(r.total_discount || 0), 0)} format="currency" description="各类优惠扣减合计" />
|
||||
<MetricCard title="实收" value={regionRows.reduce((s: number, r: any) => s + Number(r.total_received || 0), 0)} format="currency" description="扣除优惠后净收入" />
|
||||
<MetricCard title="红黄门店数" value={riskRows.filter((r: any) => r.risk_level !== '绿色').length} />
|
||||
<MetricCard title="P0+P1门店" value={p0p1.length} />
|
||||
<MetricCard title="需到店检查" value={needCheck.length} />
|
||||
@@ -90,15 +93,19 @@ export function RegionalPage() {
|
||||
data={regionRows}
|
||||
sortOptions={[
|
||||
{ key: 'region', label: '区域' },
|
||||
{ key: 'total_consumption', label: '营业收入' },
|
||||
{ key: 'total_discount', label: '优惠' },
|
||||
{ key: 'total_received', label: '实收' },
|
||||
{ key: 'avg_bill_value', label: '客单价' },
|
||||
{ key: 'store_count', label: '门店数' },
|
||||
]}
|
||||
defaultSort="total_received"
|
||||
defaultSort="total_consumption"
|
||||
defaultOrder="desc"
|
||||
columns={[
|
||||
{ key: 'region', label: '区域' },
|
||||
{ key: 'store_count', label: '门店数', align: 'right' },
|
||||
{ key: 'total_consumption', label: '营业收入', align: 'right', render: (r) => formatCurrency(r.total_consumption) },
|
||||
{ key: 'total_discount', label: '优惠', align: 'right', render: (r) => formatCurrency(r.total_discount) },
|
||||
{ key: 'total_received', label: '实收合计', align: 'right', render: (r) => formatCurrency(r.total_received) },
|
||||
{ key: 'avg_bill_value', label: '客单价', align: 'right', render: (r) => formatCurrency(r.avg_bill_value) },
|
||||
{ key: 'avg_discount_rate', label: '优惠率', align: 'right', render: (r) => formatPercent(r.avg_discount_rate) },
|
||||
|
||||
@@ -53,6 +53,7 @@ export function RevenuePage() {
|
||||
const rankRows = (storeRankingData as any)?.data || []
|
||||
|
||||
// 核心指标
|
||||
const totalConsumption = dailyRows.reduce((s: number, r: any) => s + Number(r.consumption || 0), 0)
|
||||
const totalReceived = dailyRows.reduce((s: number, r: any) => s + Number(r.received || 0), 0)
|
||||
const totalBills = dailyRows.reduce((s: number, r: any) => s + Number(r.bill_count || 0), 0)
|
||||
const totalDiscounts = dailyRows.reduce((s: number, r: any) => s + Number(r.discounts || 0), 0)
|
||||
@@ -154,11 +155,13 @@ export function RevenuePage() {
|
||||
</div>
|
||||
|
||||
{/* 核心指标 */}
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<MetricCard title="实收总额" value={totalReceived} format="currency" trend={trendReceived} description={`公式:全部门店实收合计。环比上周 ${trendReceived > 0 ? '↑' : '↓'} ${Math.abs(trendReceived)}%`} />
|
||||
<MetricCard title="账单数" value={totalBills} format="number" trend={trendBills} description={`公式:全部门店账单数合计。环比上周 ${trendBills > 0 ? '↑' : '↓'} ${Math.abs(trendBills)}%`} />
|
||||
<MetricCard title="客单价" value={avgBillValue} format="currency" description={`公式:实收 ÷ 账单数。建议值:≥ 35元 为健康`} />
|
||||
<MetricCard title="优惠总额" value={totalDiscounts} format="currency" status={avgDiscountRate > 25 ? 'bad' : avgDiscountRate > 20 ? 'warn' : 'good'} description={`优惠率 ${formatPercent(avgDiscountRate)} · 公式:优惠率 = 优惠额 ÷ (实收 + 优惠额) × 100%。建议值:≤ 18% 为健康,18-22% 需关注,> 25% 需整改`} />
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
<MetricCard title="营业收入" value={totalConsumption} format="currency" description="全部门店消费金额合计" />
|
||||
<MetricCard title="优惠总额" value={totalDiscounts} format="currency" status={avgDiscountRate > 25 ? 'bad' : avgDiscountRate > 20 ? 'warn' : 'good'} description={`优惠率 ${formatPercent(avgDiscountRate)}`} />
|
||||
<MetricCard title="实收总额" value={totalReceived} format="currency" trend={trendReceived} description={`环比上周 ${trendReceived > 0 ? '↑' : '↓'} ${Math.abs(trendReceived)}%`} />
|
||||
<MetricCard title="账单数" value={totalBills} format="number" trend={trendBills} description={`环比上周 ${trendBills > 0 ? '↑' : '↓'} ${Math.abs(trendBills)}%`} />
|
||||
<MetricCard title="客单价" value={avgBillValue} format="currency" description="实收 ÷ 账单数" />
|
||||
<MetricCard title="客流量" value={totalGuests} format="number" description={`人均 ${formatCurrency(avgGuestValue)}`} />
|
||||
</div>
|
||||
|
||||
{/* 日度营收趋势 */}
|
||||
|
||||
@@ -266,32 +266,41 @@ export function StorePage() {
|
||||
{/* KPI达成率 */}
|
||||
<KPISection month={month} level="store" storeCode={storeCode} />
|
||||
|
||||
{/* 最新经营概览 + 本周指标进度 一行 */}
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{/* 经营概览 */}
|
||||
<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>
|
||||
{anomalyCount > 0 && (
|
||||
<span className="rounded-md bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">{anomalyCount} 项异常</span>
|
||||
)}
|
||||
{/* 最新经营概览 */}
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-bold">最新经营日概览</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{card?.target_date ? `${String(card.target_date).substring(0, 10)} 当日数据 vs ${String(card.baseline_date).substring(0, 10)} 一周前同日对比` : `${month} 月末单日 vs 一周前同日对比`}
|
||||
</p>
|
||||
</div>
|
||||
{allAnomalyItems.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">暂无数据</p>
|
||||
) : (
|
||||
<div className="grid gap-2 md:grid-cols-3">
|
||||
{allAnomalyItems.map((item, idx) => (
|
||||
<div key={idx} className={`rounded-md border p-3 ${item.is_anomaly ? 'border-red-200 bg-red-50/50' : ''}`}>
|
||||
<p className="text-xs text-muted-foreground">{item.metric}</p>
|
||||
<p className={`mt-1 text-lg font-bold ${item.is_anomaly ? 'text-red-600' : ''}`}>
|
||||
{item.metric === '实收' ? formatCurrency(item.value) : formatNumber(item.value)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">日基准: {item.metric === '实收' ? formatCurrency(item.baseline) : formatNumber(item.baseline)}{item.is_anomaly && <span className="text-red-500"> ⚠</span>}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{anomalyCount > 0 && (
|
||||
<span className="rounded-md bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700">{anomalyCount} 项异常</span>
|
||||
)}
|
||||
</div>
|
||||
{allAnomalyItems.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground">暂无数据</p>
|
||||
) : (
|
||||
<div className="grid gap-2 grid-cols-5">
|
||||
{allAnomalyItems.map((item, idx) => {
|
||||
const isCurrency = item.metric === '实收' || item.metric === '营业收入' || item.metric === '优惠'
|
||||
return (
|
||||
<div key={idx} className={`rounded-md border p-3 ${item.is_anomaly ? 'border-red-200 bg-red-50/50' : ''}`}>
|
||||
<p className="text-xs text-muted-foreground">{item.metric}</p>
|
||||
<p className={`mt-1 text-lg font-bold ${item.is_anomaly ? 'text-red-600' : ''}`}>
|
||||
{isCurrency ? formatCurrency(item.value) : formatNumber(item.value)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">日基准: {isCurrency ? formatCurrency(item.baseline) : formatNumber(item.baseline)}{item.is_anomaly && <span className="text-red-500"> ⚠</span>}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 本周指标进度 */}
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
|
||||
{/* 本周指标进度条 */}
|
||||
{followup && (
|
||||
@@ -515,7 +524,9 @@ export function StorePage() {
|
||||
{sc && Number(sc.received) > 0 && (
|
||||
<>
|
||||
{/* 核心指标 - 月度汇总 */}
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4 lg:grid-cols-8">
|
||||
<MetricCard title="营业收入" value={sc.consumption} format="currency" />
|
||||
<MetricCard title="优惠" value={sc.discount} format="currency" />
|
||||
<MetricCard title="月度实收" value={sc.received} format="currency" />
|
||||
<MetricCard title="月度账单数" value={sc.bill_count} format="number" />
|
||||
<MetricCard title="客单价" value={sc.avg_bill_value} format="currency" />
|
||||
|
||||
@@ -168,9 +168,19 @@ router.get('/region/comparison', async (req: AuthRequest, res) => {
|
||||
const month = parseMonth(req)
|
||||
|
||||
const result = await query(`
|
||||
WITH store_metrics AS (
|
||||
WITH bill_region AS (
|
||||
SELECT COALESCE(ds.region, '未知区域') AS region,
|
||||
round(sum(b.consumption)::numeric, 2) AS total_consumption,
|
||||
round(sum(b.discount_total)::numeric, 2) AS total_discount
|
||||
FROM analytics.bill_fact b
|
||||
JOIN analytics.dim_store ds ON b.store_code = ds.store_code
|
||||
WHERE b.closed_at >= $1::date AND b.closed_at < ($1::date + interval '1 month')
|
||||
AND ds.region IS NOT NULL
|
||||
GROUP BY COALESCE(ds.region, '未知区域')
|
||||
),
|
||||
store_metrics AS (
|
||||
SELECT
|
||||
ds.region,
|
||||
COALESCE(ds.region, '未知区域') AS region,
|
||||
count(DISTINCT r.store_code) as store_count,
|
||||
sum(r.received) as total_revenue,
|
||||
round(avg(r.avg_bill_value)::numeric, 2) as avg_bill_value,
|
||||
@@ -180,24 +190,26 @@ router.get('/region/comparison', async (req: AuthRequest, res) => {
|
||||
FROM analytics.mv_store_risk_rating_monthly r
|
||||
JOIN analytics.dim_store ds ON r.store_code = ds.store_code
|
||||
WHERE r.month_start = $1
|
||||
AND ds.region IS NOT NULL AND ds.region != '未知区域'
|
||||
GROUP BY ds.region
|
||||
AND ds.region IS NOT NULL
|
||||
GROUP BY COALESCE(ds.region, '未知区域')
|
||||
),
|
||||
expense_metrics AS (
|
||||
SELECT
|
||||
ds.region,
|
||||
COALESCE(ds.region, '未知区域') AS region,
|
||||
avg(oe.operating_expense_rate_pct) as avg_expense_rate,
|
||||
avg(oe.rent_rate_pct) as avg_rent_rate,
|
||||
avg(oe.wage_rate_pct) as avg_wage_rate
|
||||
FROM analytics.mv_store_operating_expense_monthly oe
|
||||
JOIN analytics.dim_store ds ON oe.sales_store_code = ds.store_code
|
||||
WHERE oe.report_month = $1
|
||||
AND ds.region IS NOT NULL AND ds.region != '未知区域'
|
||||
GROUP BY ds.region
|
||||
AND ds.region IS NOT NULL
|
||||
GROUP BY COALESCE(ds.region, '未知区域')
|
||||
)
|
||||
SELECT
|
||||
sm.region,
|
||||
sm.store_count,
|
||||
br.total_consumption,
|
||||
br.total_discount,
|
||||
round(sm.total_revenue::numeric, 2) as total_revenue,
|
||||
round((sm.total_revenue / sm.store_count)::numeric, 2) as revenue_per_store,
|
||||
round(sm.avg_bill_value::numeric, 2) as avg_bill_value,
|
||||
@@ -209,6 +221,7 @@ router.get('/region/comparison', async (req: AuthRequest, res) => {
|
||||
round(em.avg_rent_rate::numeric, 2) as avg_rent_rate,
|
||||
round(em.avg_wage_rate::numeric, 2) as avg_wage_rate
|
||||
FROM store_metrics sm
|
||||
LEFT JOIN bill_region br ON sm.region = br.region
|
||||
LEFT JOIN expense_metrics em ON sm.region = em.region
|
||||
ORDER BY sm.total_revenue DESC
|
||||
`, [month])
|
||||
|
||||
@@ -9,7 +9,19 @@ const router = Router()
|
||||
router.get('/overview', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [month])
|
||||
const result = await query(`
|
||||
SELECT o.bill_count, o.received, o.avg_bill_value, o.discount_rate_pct, o.theoretical_margin_pct, o.member_bills, o.member_share_pct,
|
||||
round(b.consumption::numeric, 2) AS consumption,
|
||||
round(b.discount::numeric, 2) AS discount
|
||||
FROM analytics.mv_overview_monthly o
|
||||
LEFT JOIN (
|
||||
SELECT round(sum(consumption)::numeric, 2) AS consumption,
|
||||
round(sum(discount_total)::numeric, 2) AS discount
|
||||
FROM analytics.bill_fact
|
||||
WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
) b ON true
|
||||
WHERE o.month = $1::date
|
||||
`, [month])
|
||||
sendSuccess(res, result.rows[0])
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
@@ -120,6 +132,8 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
||||
SELECT c002 AS store_code, c003 AS store_name,
|
||||
count(*) AS bill_count,
|
||||
count(DISTINCT c176::date) AS active_days,
|
||||
round(sum(COALESCE(NULLIF(c009,'')::numeric,0)), 2) AS consumption,
|
||||
round(sum(COALESCE(NULLIF(c068,'')::numeric,0)), 2) AS discount,
|
||||
round(sum(COALESCE(NULLIF(c114,'')::numeric,0)), 2) AS received,
|
||||
round(sum(COALESCE(NULLIF(c114,'')::numeric,0)) / NULLIF(count(DISTINCT c176::date), 0), 2) AS avg_daily_received,
|
||||
round(sum(COALESCE(NULLIF(c114,'')::numeric,0)) / count(*), 2) AS avg_bill_value,
|
||||
@@ -147,7 +161,7 @@ router.get('/stores/:code', async (req: AuthRequest, res) => {
|
||||
}
|
||||
const si = storeInfo.rows[0] as any
|
||||
return sendSuccess(res, {
|
||||
scorecard: { store_code: si.store_code, store_name: si.store_name, bill_count: 0, active_days: 0, received: 0, avg_daily_received: 0, avg_bill_value: 0, avg_guest_value: 0, discount_rate_pct: 0, theoretical_margin_pct: 0, member_bill_share_pct: 0 },
|
||||
scorecard: { store_code: si.store_code, store_name: si.store_name, bill_count: 0, active_days: 0, consumption: 0, discount: 0, received: 0, avg_daily_received: 0, avg_bill_value: 0, avg_guest_value: 0, discount_rate_pct: 0, theoretical_margin_pct: 0, member_bill_share_pct: 0 },
|
||||
risk: null,
|
||||
platform: null,
|
||||
benchmark: null,
|
||||
@@ -683,7 +697,9 @@ router.get('/region/summary', async (req: AuthRequest, res) => {
|
||||
const result = await query(`
|
||||
SELECT d.region,
|
||||
count(DISTINCT b.store_code) AS store_count,
|
||||
COALESCE(sum(b.received_total), 0) AS total_received,
|
||||
round(COALESCE(sum(b.consumption), 0)::numeric, 2) AS total_consumption,
|
||||
round(COALESCE(sum(b.discount_total), 0)::numeric, 2) AS total_discount,
|
||||
round(COALESCE(sum(b.received_total), 0)::numeric, 2) AS total_received,
|
||||
count(*) AS total_bill_count,
|
||||
round(avg(b.received_total), 2) AS avg_bill_value,
|
||||
round(sum(b.discount_total) / NULLIF(sum(b.consumption), 0) * 100, 2) AS avg_discount_rate,
|
||||
@@ -754,9 +770,18 @@ router.get('/overview/profit-waterfall', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
WITH full_scope AS (
|
||||
WITH bill_discount AS (
|
||||
SELECT store_code,
|
||||
round(sum(discount_total)::numeric, 2) AS discount
|
||||
FROM analytics.bill_fact
|
||||
WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY store_code
|
||||
),
|
||||
full_scope AS (
|
||||
SELECT
|
||||
r.received,
|
||||
e.consumption,
|
||||
COALESCE(bd.discount, e.consumption - e.received) AS discount,
|
||||
e.actual_food_cost,
|
||||
e.wage_expense,
|
||||
e.rent_expense,
|
||||
@@ -770,9 +795,12 @@ router.get('/overview/profit-waterfall', async (req: AuthRequest, res) => {
|
||||
FROM analytics.mv_store_risk_rating_monthly r
|
||||
LEFT JOIN analytics.mv_store_operating_expense_monthly e
|
||||
ON r.store_code = e.sales_store_code AND e.report_month = $1::date
|
||||
LEFT JOIN bill_discount bd ON bd.store_code = r.store_code
|
||||
WHERE r.month_start = $1 AND r.received IS NOT NULL AND e.operating_expense IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
round(sum(consumption)::numeric, 2) AS consumption,
|
||||
round(sum(discount)::numeric, 2) AS discount,
|
||||
round(sum(received)::numeric, 2) AS received,
|
||||
round(sum(actual_food_cost)::numeric, 2) AS food_cost,
|
||||
round(sum(wage_expense)::numeric, 2) AS wage,
|
||||
@@ -1122,8 +1150,17 @@ router.get('/overview/profit-trend', async (req: AuthRequest, res) => {
|
||||
const startMonth = startDate.toISOString().slice(0, 10)
|
||||
|
||||
const result = await query(`
|
||||
WITH monthly AS (
|
||||
WITH bill_monthly AS (
|
||||
SELECT date_trunc('month', closed_at)::date AS report_month,
|
||||
round(sum(discount_total)::numeric, 2) AS discount
|
||||
FROM analytics.bill_fact
|
||||
WHERE closed_at >= $1::date AND closed_at < ($2::date + interval '1 month')
|
||||
GROUP BY date_trunc('month', closed_at)
|
||||
),
|
||||
monthly AS (
|
||||
SELECT e.report_month,
|
||||
round(sum(e.consumption)::numeric, 2) AS consumption,
|
||||
round(max(bm.discount)::numeric, 2) AS discount,
|
||||
round(sum(r.received)::numeric, 2) AS received,
|
||||
round(sum(e.actual_food_cost)::numeric, 2) AS food_cost,
|
||||
round(sum(e.wage_expense)::numeric, 2) AS wage,
|
||||
@@ -1134,6 +1171,7 @@ router.get('/overview/profit-trend', async (req: AuthRequest, res) => {
|
||||
FROM analytics.v_store_scorecard r
|
||||
JOIN analytics.mv_store_operating_expense_monthly e
|
||||
ON r.store_code = e.sales_store_code
|
||||
LEFT JOIN bill_monthly bm ON bm.report_month = e.report_month
|
||||
WHERE e.report_month >= $1::date AND e.report_month <= $2::date
|
||||
AND e.operating_expense IS NOT NULL AND r.received > 0
|
||||
GROUP BY e.report_month
|
||||
@@ -1248,6 +1286,7 @@ router.get('/revenue/daily-summary', async (req: AuthRequest, res) => {
|
||||
SELECT
|
||||
business_date,
|
||||
sum(bill_count)::bigint AS bill_count,
|
||||
round(sum(received + discounts)::numeric, 2) AS consumption,
|
||||
round(sum(received)::numeric, 2) AS received,
|
||||
round(sum(discounts)::numeric, 2) AS discounts,
|
||||
round(sum(discounts) / nullif(sum(received) + sum(discounts), 0) * 100, 2) AS discount_rate_pct,
|
||||
@@ -1275,15 +1314,25 @@ router.get('/bank/report', async (req: AuthRequest, res) => {
|
||||
query(`SELECT bill_count, received, avg_bill_value, discount_rate_pct, theoretical_margin_pct, member_bills, member_share_pct FROM analytics.mv_overview_monthly WHERE month = $1::date`, [month]),
|
||||
query(`SELECT business_date, bill_count, received, avg_bill_value, discount_rate_pct FROM analytics.mv_overview_daily WHERE month = $1::date ORDER BY business_date`, [month]),
|
||||
query(`
|
||||
WITH full_scope AS (
|
||||
SELECT r.received, e.actual_food_cost AS food_cost, e.wage_expense AS wage, e.rent_expense AS rent,
|
||||
WITH bill_discount AS (
|
||||
SELECT store_code,
|
||||
round(sum(discount_total)::numeric, 2) AS discount
|
||||
FROM analytics.bill_fact
|
||||
WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY store_code
|
||||
),
|
||||
full_scope AS (
|
||||
SELECT r.received, e.consumption, COALESCE(bd.discount, e.consumption - e.received) AS discount,
|
||||
e.actual_food_cost AS food_cost, e.wage_expense AS wage, e.rent_expense AS rent,
|
||||
e.utility_expense AS utility, e.dorm_expense AS dorm, e.delivery_commission_expense AS commission,
|
||||
e.card_fee_expense AS card_fee, e.repair_clean_expense AS repair, e.operating_expense AS operating_expense
|
||||
FROM analytics.mv_store_risk_rating_monthly r
|
||||
LEFT JOIN analytics.mv_store_operating_expense_monthly e ON r.store_code = e.sales_store_code AND e.report_month = $1::date
|
||||
LEFT JOIN bill_discount bd ON bd.store_code = r.store_code
|
||||
WHERE r.received IS NOT NULL AND e.operating_expense IS NOT NULL
|
||||
)
|
||||
SELECT round(sum(received)::numeric,2) AS received, round(sum(food_cost)::numeric,2) AS food_cost, round(sum(wage)::numeric,2) AS wage,
|
||||
SELECT round(sum(consumption)::numeric,2) AS consumption, round(sum(discount)::numeric,2) AS discount,
|
||||
round(sum(received)::numeric,2) AS received, round(sum(food_cost)::numeric,2) AS food_cost, round(sum(wage)::numeric,2) AS wage,
|
||||
round(sum(rent)::numeric,2) AS rent, round(sum(utility)::numeric,2) AS utility, round(sum(dorm)::numeric,2) AS dorm,
|
||||
round(sum(commission)::numeric,2) AS commission, round(sum(card_fee+repair)::numeric,2) AS other_expense,
|
||||
round(sum(operating_expense)::numeric,2) AS total_expense,
|
||||
@@ -1338,7 +1387,7 @@ router.get('/bank/report', async (req: AuthRequest, res) => {
|
||||
sendSuccess(res, {
|
||||
overview: { ...ov, received: f(ov?.received), bill_count: i(ov?.bill_count), avg_bill_value: f(ov?.avg_bill_value), discount_rate_pct: f(ov?.discount_rate_pct), theoretical_margin_pct: f(ov?.theoretical_margin_pct), member_bills: i(ov?.member_bills), member_share_pct: f(ov?.member_share_pct) },
|
||||
daily: dailyRows,
|
||||
waterfall: { ...wf, received: f(wf?.received), food_cost: f(wf?.food_cost), wage: f(wf?.wage), rent: f(wf?.rent), utility: f(wf?.utility), dorm: f(wf?.dorm), commission: f(wf?.commission), other_expense: f(wf?.other_expense), total_expense: f(wf?.total_expense), store_contribution: f(wf?.store_contribution) },
|
||||
waterfall: { ...wf, consumption: f(wf?.consumption), discount: f(wf?.discount), received: f(wf?.received), food_cost: f(wf?.food_cost), wage: f(wf?.wage), rent: f(wf?.rent), utility: f(wf?.utility), dorm: f(wf?.dorm), commission: f(wf?.commission), other_expense: f(wf?.other_expense), total_expense: f(wf?.total_expense), store_contribution: f(wf?.store_contribution) },
|
||||
risk: riskRows,
|
||||
channel: channelTotals,
|
||||
stores: storeRows,
|
||||
|
||||
@@ -418,7 +418,7 @@ router.get('/forecast', async (req: AuthRequest, res) => {
|
||||
count(*) AS bills
|
||||
FROM bill_records
|
||||
WHERE c175 IS NOT NULL AND c175 != ''
|
||||
AND c175 >= $1 AND c175 < $1::date + interval '1 month'
|
||||
AND c175::timestamp >= $1::date AND c175::timestamp < ($1::date + interval '1 month')
|
||||
GROUP BY hour, day_type, bill_date
|
||||
),
|
||||
hourly_stats AS (
|
||||
|
||||
@@ -12,11 +12,20 @@ router.get('/overview', async (req: AuthRequest, res) => {
|
||||
try {
|
||||
const month = parseMonth(req)
|
||||
const result = await query(`
|
||||
WITH full_scope AS (
|
||||
WITH bill_discount AS (
|
||||
SELECT store_code,
|
||||
round(sum(discount_total)::numeric, 2) AS discount
|
||||
FROM analytics.bill_fact
|
||||
WHERE closed_at >= $1::date AND closed_at < ($1::date + interval '1 month')
|
||||
GROUP BY store_code
|
||||
),
|
||||
full_scope AS (
|
||||
SELECT
|
||||
r.store_code,
|
||||
r.store_name,
|
||||
r.received,
|
||||
e.consumption,
|
||||
COALESCE(bd.discount, e.consumption - e.received) AS discount,
|
||||
r.bill_count,
|
||||
r.theoretical_margin_pct,
|
||||
e.operating_expense,
|
||||
@@ -35,6 +44,7 @@ router.get('/overview', async (req: AuthRequest, res) => {
|
||||
FROM analytics.mv_store_risk_rating_monthly r
|
||||
LEFT JOIN analytics.mv_store_operating_expense_monthly e
|
||||
ON r.store_code = e.sales_store_code AND e.report_month = $1::date
|
||||
LEFT JOIN bill_discount bd ON bd.store_code = r.store_code
|
||||
WHERE r.month_start = $1 AND r.received IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
@@ -43,6 +53,8 @@ router.get('/overview', async (req: AuthRequest, res) => {
|
||||
count(*) FILTER (WHERE actual_store_contribution <= 0 AND received > 0 AND actual_store_contribution IS NOT NULL) AS loss_stores,
|
||||
count(*) FILTER (WHERE operating_expense IS NULL) AS no_expense_stores,
|
||||
sum(bill_count)::bigint AS total_bills,
|
||||
round(sum(consumption)::numeric, 2) AS total_consumption,
|
||||
round(sum(discount)::numeric, 2) AS total_discount,
|
||||
round(sum(received)::numeric, 2) AS total_received,
|
||||
round(sum(received) / nullif(sum(bill_count), 0), 2) AS avg_bill_value,
|
||||
round(sum(operating_expense)::numeric, 2) AS total_expense,
|
||||
|
||||
@@ -313,6 +313,8 @@ router.get('/stores/:code/daily-card', async (req: AuthRequest, res) => {
|
||||
),
|
||||
today_stats AS (
|
||||
SELECT c003 AS store_name,
|
||||
round(sum(COALESCE(NULLIF(c009,'')::numeric,0)), 2) AS consumption,
|
||||
round(sum(COALESCE(NULLIF(c068,'')::numeric,0)), 2) AS discount,
|
||||
round(sum(${RECEIVED_COLS}), 2) AS received,
|
||||
count(*) AS bill_count,
|
||||
round(sum(${RECEIVED_COLS}) / count(*), 2) AS avg_bill
|
||||
@@ -322,6 +324,8 @@ router.get('/stores/:code/daily-card', async (req: AuthRequest, res) => {
|
||||
),
|
||||
week_ago_stats AS (
|
||||
SELECT
|
||||
round(sum(COALESCE(NULLIF(c009,'')::numeric,0)), 2) AS consumption,
|
||||
round(sum(COALESCE(NULLIF(c068,'')::numeric,0)), 2) AS discount,
|
||||
round(sum(${RECEIVED_COLS}), 2) AS received,
|
||||
count(*) AS bill_count,
|
||||
round(sum(${RECEIVED_COLS}) / count(*), 2) AS avg_bill
|
||||
@@ -330,12 +334,16 @@ router.get('/stores/:code/daily-card', async (req: AuthRequest, res) => {
|
||||
GROUP BY c003
|
||||
)
|
||||
SELECT t.store_name, '收入' AS module,
|
||||
td.business_date AS target_date,
|
||||
(td.business_date - interval '7 days')::date AS baseline_date,
|
||||
jsonb_build_array(
|
||||
jsonb_build_object('metric', '营业收入', 'value', t.consumption, 'baseline', COALESCE(w.consumption, 0), 'is_anomaly', t.consumption < (COALESCE(w.consumption, 0) * 0.8)),
|
||||
jsonb_build_object('metric', '优惠', 'value', t.discount, 'baseline', COALESCE(w.discount, 0), 'is_anomaly', t.discount > (COALESCE(w.discount, 0) * 1.2)),
|
||||
jsonb_build_object('metric', '实收', 'value', t.received, 'baseline', COALESCE(w.received, 0), 'is_anomaly', t.received < (COALESCE(w.received, 0) * 0.8)),
|
||||
jsonb_build_object('metric', '账单数', 'value', t.bill_count, 'baseline', COALESCE(w.bill_count, 0), 'is_anomaly', t.bill_count::numeric < (COALESCE(w.bill_count, 0) * 0.8)),
|
||||
jsonb_build_object('metric', '客单价', 'value', t.avg_bill, 'baseline', COALESCE(w.avg_bill, 0), 'is_anomaly', t.avg_bill < (COALESCE(w.avg_bill, 0) * 0.9))
|
||||
) AS anomalies
|
||||
FROM today_stats t
|
||||
FROM today_stats t, target_day td
|
||||
LEFT JOIN week_ago_stats w ON true
|
||||
`, params)
|
||||
const tasks = await query(`
|
||||
@@ -343,7 +351,9 @@ router.get('/stores/:code/daily-card', async (req: AuthRequest, res) => {
|
||||
WHERE t.store_code = $1 AND t.status IN ('待启动', '进行中')
|
||||
ORDER BY t.priority LIMIT 3
|
||||
`, [code])
|
||||
sendSuccess(res, { anomalies: result.rows, todos: tasks.rows })
|
||||
const targetDate = result.rows[0]?.target_date
|
||||
const baselineDate = result.rows[0]?.baseline_date
|
||||
sendSuccess(res, { anomalies: result.rows, todos: tasks.rows, target_date: targetDate, baseline_date: baselineDate })
|
||||
} catch (err: any) {
|
||||
sendError(res, err.message)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user