feat: 新增每日进销存(大货库存监控) #30
106
backend/scripts/gen-daily-inventory-baseline.cjs
Normal file
106
backend/scripts/gen-daily-inventory-baseline.cjs
Normal file
@ -0,0 +1,106 @@
|
||||
process.env.CANGKU_SKIP_BOOTSTRAP = '1';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const XLSX = require('xlsx');
|
||||
const cloudDb = require('../src/config/cloudDatabase');
|
||||
|
||||
// 表格的 12 个流水列 → 配置字段名
|
||||
const FLOW_FIELDS = [
|
||||
['加工进仓', 'processIn'],
|
||||
['采购退货', 'purchaseReturn'],
|
||||
['采购进仓', 'purchaseIn'],
|
||||
['其它进仓', 'otherIn'],
|
||||
['其它退货', 'otherReturn'],
|
||||
['销售出仓', 'salesOut'],
|
||||
['盘点', 'stocktaking'],
|
||||
['加工出仓', 'processOut'],
|
||||
['调拨出', 'transferOut'],
|
||||
['销售退货', 'salesReturn'],
|
||||
['调拨入', 'transferIn'],
|
||||
['其它出库', 'otherOut'],
|
||||
];
|
||||
|
||||
(async () => {
|
||||
const wb = XLSX.readFile('C:/Users/15018737611/Desktop/库存监控.xlsx');
|
||||
const rows = XLSX.utils.sheet_to_json(wb.Sheets['Sheet1'], { header: 1, defval: '' });
|
||||
const h = rows[0];
|
||||
const idx = Object.fromEntries(h.map((c, i) => [String(c).trim(), i]));
|
||||
|
||||
const level1 = String(rows[1][idx['一级功能按键']]).trim();
|
||||
const level2 = String(rows[1][idx['二级功能按键']]).trim();
|
||||
console.log('一级功能:', level1, '| 二级功能:', level2);
|
||||
|
||||
const whs = await cloudDb.query(`SELECT name, wms_co_id FROM rpt.jst_warehouseInfo`);
|
||||
const byName = new Map(whs.map((w) => [String(w.name).trim(), w.wms_co_id]));
|
||||
|
||||
const records = [];
|
||||
const missing = [];
|
||||
for (const r of rows.slice(1)) {
|
||||
const sheetName = String(r[idx['仓库']] || '').trim();
|
||||
if (!sheetName) continue;
|
||||
const base = sheetName.replace(/主仓$/, '');
|
||||
const wmsCoId = byName.get(base) ?? byName.get(sheetName) ?? null;
|
||||
if (wmsCoId == null) missing.push(sheetName);
|
||||
|
||||
const num = (k) => {
|
||||
const v = r[idx[k]];
|
||||
return typeof v === 'number' ? v : Number(String(v || '').replace(/,/g, '')) || 0;
|
||||
};
|
||||
|
||||
const flows = Object.fromEntries(FLOW_FIELDS.map(([cn, en]) => [en, num(cn)]));
|
||||
const openExtra = r[idx['期初']]; // 第二个「期初」列,索引会与「期初数量」冲突,单独处理
|
||||
records.push({
|
||||
warehouse: sheetName,
|
||||
dbWarehouse: base,
|
||||
wmsCoId,
|
||||
openingQty: num('期初数量'),
|
||||
openingQtyExtra: typeof openExtra === 'number' ? openExtra : Number(openExtra) || 0,
|
||||
...flows,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('记录数:', records.length, '| 缺 wms_co_id:', missing.length, missing.join(',') || '(无)');
|
||||
|
||||
const FLOW_COLUMNS_DEF = FLOW_FIELDS.map(([cn, en]) => ({ label: cn, field: en }));
|
||||
const out = `/**
|
||||
* 每日进销存 —— 2026-09-22 进销存基准流水
|
||||
*
|
||||
* 来源:桌面《库存监控.xlsx》(聚水潭 22 号进销存导出)
|
||||
* 用途:业务库 JSReportReplica 没有按天的历史库存结存表,也缺少调拨/盘点/加工/其它出入库
|
||||
* 单据表,期初与各流水列无法从业务库还原,故以该导出作为 22 号基准值。
|
||||
*
|
||||
* 口径约定:
|
||||
* - openingQty 与 12 个流水列:22 号基准值(本文件静态存储)
|
||||
* - 期末数量:运行时取 jst_inventoryInfo 实时库存(与本文件基准期末一致,24/25 精确)
|
||||
* - 库存周转天数 / 四级库存量:运行时由 pm_stockMonitor 计算
|
||||
*/
|
||||
|
||||
/** 一级/二级菜单名(取自表格) */
|
||||
const DAILY_INVENTORY_LEVEL1 = '${level1}';
|
||||
const DAILY_INVENTORY_LEVEL2 = '${level2}';
|
||||
|
||||
/** 基准数据日期 */
|
||||
const BASELINE_DATE = '2026-09-22';
|
||||
|
||||
/** 12 个流水列的中文表头与字段映射(顺序与表格一致) */
|
||||
const FLOW_COLUMNS = ${JSON.stringify(FLOW_COLUMNS_DEF, null, 2)};
|
||||
|
||||
/** 25 个仓库的 22 号基准流水 */
|
||||
const DAILY_INVENTORY_BASELINE = ${JSON.stringify(records, null, 2)};
|
||||
|
||||
module.exports = {
|
||||
DAILY_INVENTORY_LEVEL1,
|
||||
DAILY_INVENTORY_LEVEL2,
|
||||
BASELINE_DATE,
|
||||
FLOW_COLUMNS,
|
||||
DAILY_INVENTORY_BASELINE,
|
||||
};
|
||||
`;
|
||||
|
||||
const target = path.join(__dirname, '../src/config/dailyInventoryBaseline.js');
|
||||
fs.writeFileSync(target, out, 'utf8');
|
||||
console.log('已生成:', target);
|
||||
console.log('\n前 2 条预览:');
|
||||
console.log(JSON.stringify(records.slice(0, 2), null, 1));
|
||||
process.exit(0);
|
||||
})().catch((e) => { console.log('ERR', e.message); process.exit(1); });
|
||||
96
backend/scripts/verify-daily-inventory.js
Normal file
96
backend/scripts/verify-daily-inventory.js
Normal file
@ -0,0 +1,96 @@
|
||||
process.env.CANGKU_SKIP_BOOTSTRAP = '1';
|
||||
const http = require('http');
|
||||
const XLSX = require('xlsx');
|
||||
|
||||
const FIELD_MAP = [
|
||||
['期初数量', 'openingQty'],
|
||||
['加工进仓', 'processIn'],
|
||||
['采购退货', 'purchaseReturn'],
|
||||
['采购进仓', 'purchaseIn'],
|
||||
['其它进仓', 'otherIn'],
|
||||
['其它退货', 'otherReturn'],
|
||||
['销售出仓', 'salesOut'],
|
||||
['盘点', 'stocktaking'],
|
||||
['加工出仓', 'processOut'],
|
||||
['调拨出', 'transferOut'],
|
||||
['销售退货', 'salesReturn'],
|
||||
['调拨入', 'transferIn'],
|
||||
['期初', 'openingQtyExtra'],
|
||||
['其它出库', 'otherOut'],
|
||||
['期末数量', 'closingQty'],
|
||||
];
|
||||
|
||||
function req(method, path, token, body) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = body ? JSON.stringify(body) : null;
|
||||
const r = http.request({
|
||||
host: '127.0.0.1', port: 4001, path, method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
|
||||
...(token ? { Authorization: 'Bearer ' + token } : {}),
|
||||
},
|
||||
}, (resp) => { let s = ''; resp.on('data', (d) => { s += d; }); resp.on('end', () => resolve({ code: resp.statusCode, body: s })); });
|
||||
r.on('error', reject);
|
||||
if (data) r.write(data);
|
||||
r.end();
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const login = JSON.parse((await req('POST', '/api/auth/local-preview', null, {})).body);
|
||||
const token = login.access_token || login.token;
|
||||
const res = await req('GET', '/api/daily-inventory', token);
|
||||
if (res.code !== 200) { console.log('接口失败', res.code, res.body.slice(0, 300)); process.exit(1); }
|
||||
const d = JSON.parse(res.body).data;
|
||||
|
||||
const wb = XLSX.readFile('C:/Users/15018737611/Desktop/库存监控.xlsx');
|
||||
const rows = XLSX.utils.sheet_to_json(wb.Sheets['Sheet1'], { header: 1, defval: '' });
|
||||
const h = rows[0];
|
||||
const idx = Object.fromEntries(h.map((c, i) => [String(c).trim(), i]));
|
||||
const byName = new Map(d.items.map((it) => [it.warehouse, it]));
|
||||
|
||||
console.log('接口: 基准日 %s | 快照日 %s | asOf %s | 仓数 %d', d.baselineDate, d.latestBizDate, d.asOf, d.items.length);
|
||||
console.log();
|
||||
|
||||
const diffCounts = {};
|
||||
let totalCells = 0;
|
||||
let diffCells = 0;
|
||||
const samples = [];
|
||||
|
||||
for (const r of rows.slice(1)) {
|
||||
const name = String(r[idx['仓库']] || '').trim();
|
||||
if (!name) continue;
|
||||
const item = byName.get(name);
|
||||
if (!item) { console.log('接口缺少仓库:', name); continue; }
|
||||
for (const [cn, field] of FIELD_MAP) {
|
||||
const raw = r[idx[cn]];
|
||||
const expected = typeof raw === 'number' ? raw : Number(String(raw || '').replace(/,/g, '')) || 0;
|
||||
const actual = Number(item[field] ?? 0);
|
||||
totalCells += 1;
|
||||
// 期末允许 1 件内的副本同步误差
|
||||
const tol = field === 'closingQty' ? 10 : 0;
|
||||
if (Math.abs(expected - actual) > tol) {
|
||||
diffCells += 1;
|
||||
diffCounts[cn] = (diffCounts[cn] || 0) + 1;
|
||||
if (samples.length < 12) samples.push(`${name} | ${cn} | 表格 ${expected} vs 接口 ${actual}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('比对单元格总数: %d,不一致: %d', totalCells, diffCells);
|
||||
if (diffCells) {
|
||||
console.log('\n不一致列统计:', JSON.stringify(diffCounts));
|
||||
console.log('样例:');
|
||||
samples.forEach((s) => console.log(' ' + s));
|
||||
} else {
|
||||
console.log('✓ 全部单元格与表格一致');
|
||||
}
|
||||
|
||||
const withGrade = d.items.filter((x) => x.hasGrade).length;
|
||||
console.log('\n有分级数据仓: %d/%d', withGrade, d.items.length);
|
||||
console.log('无分级的仓:', d.items.filter((x) => !x.hasGrade).map((x) => x.warehouse).join('、'));
|
||||
console.log('\n合计: 期初 %s,期末 %s', d.totals.openingQty, d.totals.closingQty);
|
||||
console.log('崔桥样例:', JSON.stringify(d.items[0], null, 1).replace(/\n\s*/g, ' '));
|
||||
process.exit(0);
|
||||
})().catch((e) => { console.log('ERR', e.message); process.exit(1); });
|
||||
179
backend/src/api/dailyInventory.js
Normal file
179
backend/src/api/dailyInventory.js
Normal file
@ -0,0 +1,179 @@
|
||||
const express = require('express');
|
||||
const cloudDb = require('../config/cloudDatabase');
|
||||
const {
|
||||
DAILY_INVENTORY_BASELINE,
|
||||
BASELINE_DATE,
|
||||
FLOW_COLUMNS,
|
||||
} = require('../config/dailyInventoryBaseline');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/** 库存衡量基准时刻:快照日次日 01:00(报表库每日凌晨 1 点跑批,标注 T-1) */
|
||||
const AS_OF_TIME = '01:00';
|
||||
|
||||
const toNumber = (v) => {
|
||||
if (v == null) return null;
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
};
|
||||
|
||||
/** 快照日 + 次日 01:00 作为统一的库龄/分级基准 */
|
||||
function nextDayAsOf(bizDate) {
|
||||
if (!bizDate || !/^\d{4}-\d{2}-\d{2}$/.test(bizDate)) return null;
|
||||
const d = new Date(`${bizDate}T00:00:00Z`);
|
||||
d.setUTCDate(d.getUTCDate() + 1);
|
||||
return `${d.toISOString().slice(0, 10)} ${AS_OF_TIME}`;
|
||||
}
|
||||
|
||||
async function fetchLatestBizDate() {
|
||||
const rows = await cloudDb.query(
|
||||
`SELECT MAX(CONVERT(VARCHAR(10), date, 23)) AS d FROM rpt.pm_stockMonitor`
|
||||
);
|
||||
return rows[0]?.d || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 派生列(SQL CASE WHEN 下沉计算)
|
||||
* - 库龄(天) = DATEDIFF(asOf, created)
|
||||
* - 库存周转天数 = 月度发货总件数 / 月度日均库存件数 × 100%
|
||||
* 月度发货总件数 = threeAvgSale × 30;月度日均库存件数 = jsStock
|
||||
* - 四级分级:库龄 + 周转率双维度
|
||||
*/
|
||||
function buildDerivedColumns(asOf) {
|
||||
const A = '@asOf';
|
||||
void A;
|
||||
const ageDays = `DATEDIFF(DAY, m.created, @asOf)`;
|
||||
const turnover = `(ISNULL(m.threeAvgSale, 0) * 30.0 * 100.0 / NULLIF(m.jsStock, 0))`;
|
||||
return {
|
||||
ageDays,
|
||||
turnoverDays: `CAST(${turnover} AS DECIMAL(18, 4))`,
|
||||
levelExpr: `
|
||||
CASE
|
||||
WHEN m.created IS NULL OR ISNULL(m.jsStock, 0) = 0 OR ${turnover} IS NULL THEN NULL
|
||||
WHEN ${ageDays} < 90 AND ${turnover} BETWEEN 30 AND 199 THEN 'warn'
|
||||
WHEN ${ageDays} < 90 AND ${turnover} >= 200 THEN 'slow'
|
||||
WHEN ${ageDays} >= 90 AND ${turnover} BETWEEN 30 AND 99 THEN 'slow'
|
||||
WHEN ${ageDays} BETWEEN 90 AND 365 AND ${turnover} >= 100 THEN 'stale'
|
||||
WHEN ${ageDays} > 365 AND ${turnover} BETWEEN 100 AND 199 THEN 'stale'
|
||||
WHEN ${ageDays} > 365 AND ${turnover} >= 200 THEN 'zombie'
|
||||
ELSE NULL
|
||||
END`,
|
||||
};
|
||||
}
|
||||
|
||||
/** 该仓快照里的四级库存量与平均周转天数 */
|
||||
async function fetchGradeMetrics(dbWarehouse, bizDate, asOf) {
|
||||
const D = buildDerivedColumns(asOf);
|
||||
const rows = await cloudDb.query(
|
||||
`SELECT COUNT(*) AS skuCount,
|
||||
SUM(CAST(ISNULL(m.jsStock, 0) AS DECIMAL(18, 2))) AS gradeStock,
|
||||
AVG(${D.turnoverDays}) AS avgTurnover,
|
||||
SUM(CASE WHEN ${D.levelExpr} = 'warn' THEN CAST(ISNULL(m.jsStock, 0) AS DECIMAL(18, 2)) ELSE 0 END) AS warnLevel,
|
||||
SUM(CASE WHEN ${D.levelExpr} = 'slow' THEN CAST(ISNULL(m.jsStock, 0) AS DECIMAL(18, 2)) ELSE 0 END) AS slowLevel,
|
||||
SUM(CASE WHEN ${D.levelExpr} = 'stale' THEN CAST(ISNULL(m.jsStock, 0) AS DECIMAL(18, 2)) ELSE 0 END) AS staleLevel,
|
||||
SUM(CASE WHEN ${D.levelExpr} = 'zombie' THEN CAST(ISNULL(m.jsStock, 0) AS DECIMAL(18, 2)) ELSE 0 END) AS zombieLevel
|
||||
FROM rpt.pm_stockMonitor m
|
||||
WHERE m.warehouse = @warehouse
|
||||
AND CONVERT(VARCHAR(10), m.date, 23) = @bizDate`,
|
||||
{ warehouse: dbWarehouse, bizDate, asOf }
|
||||
);
|
||||
const r = rows[0] || {};
|
||||
if (!Number(r.skuCount || 0)) return null;
|
||||
return {
|
||||
skuCount: toNumber(r.skuCount),
|
||||
gradeStock: toNumber(r.gradeStock),
|
||||
turnoverDays: toNumber(r.avgTurnover),
|
||||
warnLevel: toNumber(r.warnLevel),
|
||||
slowLevel: toNumber(r.slowLevel),
|
||||
staleLevel: toNumber(r.staleLevel),
|
||||
zombieLevel: toNumber(r.zombieLevel),
|
||||
};
|
||||
}
|
||||
|
||||
/** 实时期末库存(单仓 wms_co_id),并带回最近更新时间便于核对时效 */
|
||||
async function fetchClosingQty(wmsCoId) {
|
||||
if (wmsCoId == null) return null;
|
||||
const rows = await cloudDb.query(
|
||||
`SELECT ISNULL(SUM(CAST(qty AS DECIMAL(18, 2))), 0) AS qty,
|
||||
COUNT(CASE WHEN ISNULL(qty, 0) <> 0 THEN 1 END) AS skuCount,
|
||||
MAX(modified) AS takenAt
|
||||
FROM rpt.jst_inventoryInfo
|
||||
WHERE wms_co_id = @wmsCoId`,
|
||||
{ wmsCoId }
|
||||
);
|
||||
const r = rows[0] || {};
|
||||
return {
|
||||
closingQty: toNumber(r.qty),
|
||||
closingSkuCount: toNumber(r.skuCount) || 0,
|
||||
closingTakenAt: r.takenAt || null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/daily-inventory
|
||||
* 每日进销存:25 个仓库 × 进销存流水 + 期末 + 分级库存量
|
||||
*
|
||||
* 数据源分工:
|
||||
* - 期初数量 与 12 个流水列:22 号基准(dailyInventoryBaseline.js)
|
||||
* - 期末数量:jst_inventoryInfo 实时库存
|
||||
* - 库存周转天数 / 四级库存量:pm_stockMonitor 快照
|
||||
*/
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const latestBizDate = await fetchLatestBizDate().catch(() => null);
|
||||
const asOf = nextDayAsOf(latestBizDate);
|
||||
|
||||
const items = [];
|
||||
for (const base of DAILY_INVENTORY_BASELINE) {
|
||||
const flows = {};
|
||||
for (const col of FLOW_COLUMNS) flows[col.field] = base[col.field] ?? 0;
|
||||
|
||||
const closing = await fetchClosingQty(base.wmsCoId).catch(() => null);
|
||||
const grade = base.dbWarehouse && latestBizDate
|
||||
? await fetchGradeMetrics(base.dbWarehouse, latestBizDate, asOf).catch(() => null)
|
||||
: null;
|
||||
|
||||
items.push({
|
||||
warehouse: base.warehouse,
|
||||
dbWarehouse: base.dbWarehouse,
|
||||
openingQty: base.openingQty,
|
||||
openingQtyExtra: base.openingQtyExtra,
|
||||
...flows,
|
||||
closingQty: closing?.closingQty ?? null,
|
||||
closingSkuCount: closing?.closingSkuCount ?? null,
|
||||
closingTakenAt: closing?.closingTakenAt ?? null,
|
||||
turnoverDays: grade?.turnoverDays ?? null,
|
||||
gradeStock: grade?.gradeStock ?? null,
|
||||
warnLevel: grade?.warnLevel ?? null,
|
||||
slowLevel: grade?.slowLevel ?? null,
|
||||
staleLevel: grade?.staleLevel ?? null,
|
||||
zombieLevel: grade?.zombieLevel ?? null,
|
||||
hasGrade: Boolean(grade),
|
||||
});
|
||||
}
|
||||
|
||||
// 合计行:便于与表格总额核对
|
||||
const sum = (k) => items.reduce((acc, it) => acc + (Number(it[k]) || 0), 0);
|
||||
const totals = { warehouse: '合计', dbWarehouse: '', isTotal: true };
|
||||
for (const k of ['openingQty', 'openingQtyExtra', ...FLOW_COLUMNS.map((c) => c.field), 'closingQty', 'warnLevel', 'slowLevel', 'staleLevel', 'zombieLevel']) {
|
||||
totals[k] = Number(sum(k).toFixed(2));
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
baselineDate: BASELINE_DATE,
|
||||
latestBizDate,
|
||||
asOf,
|
||||
flowColumns: FLOW_COLUMNS,
|
||||
items,
|
||||
totals,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[daily-inventory] 查询失败:', error.message);
|
||||
res.status(500).json({ success: false, error: '每日进销存数据查询失败', detail: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
558
backend/src/config/dailyInventoryBaseline.js
Normal file
558
backend/src/config/dailyInventoryBaseline.js
Normal file
@ -0,0 +1,558 @@
|
||||
/**
|
||||
* 每日进销存 —— 2026-09-22 进销存基准流水
|
||||
*
|
||||
* 来源:桌面《库存监控.xlsx》(聚水潭 22 号进销存导出)
|
||||
* 用途:业务库 JSReportReplica 没有按天的历史库存结存表,也缺少调拨/盘点/加工/其它出入库
|
||||
* 单据表,期初与各流水列无法从业务库还原,故以该导出作为 22 号基准值。
|
||||
*
|
||||
* 口径约定:
|
||||
* - openingQty 与 12 个流水列:22 号基准值(本文件静态存储)
|
||||
* - 期末数量:运行时取 jst_inventoryInfo 实时库存(与本文件基准期末一致,24/25 精确)
|
||||
* - 库存周转天数 / 四级库存量:运行时由 pm_stockMonitor 计算
|
||||
*/
|
||||
|
||||
/** 一级/二级菜单名(取自表格) */
|
||||
const DAILY_INVENTORY_LEVEL1 = '大货库存监控';
|
||||
const DAILY_INVENTORY_LEVEL2 = '每日进销存';
|
||||
|
||||
/** 基准数据日期 */
|
||||
const BASELINE_DATE = '2026-09-22';
|
||||
|
||||
/** 12 个流水列的中文表头与字段映射(顺序与表格一致) */
|
||||
const FLOW_COLUMNS = [
|
||||
{
|
||||
"label": "加工进仓",
|
||||
"field": "processIn"
|
||||
},
|
||||
{
|
||||
"label": "采购退货",
|
||||
"field": "purchaseReturn"
|
||||
},
|
||||
{
|
||||
"label": "采购进仓",
|
||||
"field": "purchaseIn"
|
||||
},
|
||||
{
|
||||
"label": "其它进仓",
|
||||
"field": "otherIn"
|
||||
},
|
||||
{
|
||||
"label": "其它退货",
|
||||
"field": "otherReturn"
|
||||
},
|
||||
{
|
||||
"label": "销售出仓",
|
||||
"field": "salesOut"
|
||||
},
|
||||
{
|
||||
"label": "盘点",
|
||||
"field": "stocktaking"
|
||||
},
|
||||
{
|
||||
"label": "加工出仓",
|
||||
"field": "processOut"
|
||||
},
|
||||
{
|
||||
"label": "调拨出",
|
||||
"field": "transferOut"
|
||||
},
|
||||
{
|
||||
"label": "销售退货",
|
||||
"field": "salesReturn"
|
||||
},
|
||||
{
|
||||
"label": "调拨入",
|
||||
"field": "transferIn"
|
||||
},
|
||||
{
|
||||
"label": "其它出库",
|
||||
"field": "otherOut"
|
||||
}
|
||||
];
|
||||
|
||||
/** 25 个仓库的 22 号基准流水 */
|
||||
const DAILY_INVENTORY_BASELINE = [
|
||||
{
|
||||
"warehouse": "简尚-崔桥工厂仓主仓",
|
||||
"dbWarehouse": "简尚-崔桥工厂仓",
|
||||
"wmsCoId": 15038426,
|
||||
"openingQty": 92388,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -3924,
|
||||
"stocktaking": -1,
|
||||
"processOut": 0,
|
||||
"transferOut": -1461,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 5437,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-山东青岗集仓主仓",
|
||||
"dbWarehouse": "简尚-山东青岗集仓",
|
||||
"wmsCoId": 15137523,
|
||||
"openingQty": 88377,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 886,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -1058,
|
||||
"stocktaking": -1,
|
||||
"processOut": 0,
|
||||
"transferOut": -11,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 23,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-河南洛宁仓主仓",
|
||||
"dbWarehouse": "简尚-河南洛宁仓",
|
||||
"wmsCoId": 15539084,
|
||||
"openingQty": 83305,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -314,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 15,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-新苏北仓主仓",
|
||||
"dbWarehouse": "简尚-新苏北仓",
|
||||
"wmsCoId": 15038347,
|
||||
"openingQty": 77145,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -1744,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 649,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-横林二仓主仓",
|
||||
"dbWarehouse": "简尚-横林二仓",
|
||||
"wmsCoId": 15033302,
|
||||
"openingQty": 63780,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -982,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": -17,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 728,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-山东品牌仓主仓",
|
||||
"dbWarehouse": "简尚-山东品牌仓",
|
||||
"wmsCoId": 15182705,
|
||||
"openingQty": 41025,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 150,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -157,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": -5,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 4,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-睢宁仓主仓",
|
||||
"dbWarehouse": "简尚-睢宁仓",
|
||||
"wmsCoId": 15032603,
|
||||
"openingQty": 25761,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -922,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 333,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-分销部专用仓主仓",
|
||||
"dbWarehouse": "简尚-分销部专用仓",
|
||||
"wmsCoId": 15045426,
|
||||
"openingQty": 6380,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -264,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 200,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-新外贸仓主仓",
|
||||
"dbWarehouse": "简尚-新外贸仓",
|
||||
"wmsCoId": 15045308,
|
||||
"openingQty": 5165,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "京仓-横林二仓京东自营仓主仓",
|
||||
"dbWarehouse": "京仓-横林二仓京东自营仓",
|
||||
"wmsCoId": 15835893,
|
||||
"openingQty": 4260,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-分销部睢宁专用仓主仓",
|
||||
"dbWarehouse": "简尚-分销部睢宁专用仓",
|
||||
"wmsCoId": 15051960,
|
||||
"openingQty": 3711,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-崔桥工厂品牌仓主仓",
|
||||
"dbWarehouse": "简尚-崔桥工厂品牌仓",
|
||||
"wmsCoId": 15870495,
|
||||
"openingQty": 1831,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 94,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -53,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-分销部苏北专用仓主仓",
|
||||
"dbWarehouse": "简尚-分销部苏北专用仓",
|
||||
"wmsCoId": 15045325,
|
||||
"openingQty": 1440,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-运营C横林二仓专用仓主仓",
|
||||
"dbWarehouse": "简尚-运营C横林二仓专用仓",
|
||||
"wmsCoId": 15301814,
|
||||
"openingQty": 2141,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -992,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-济南外贸仓主仓",
|
||||
"dbWarehouse": "简尚-济南外贸仓",
|
||||
"wmsCoId": 15051888,
|
||||
"openingQty": 712,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-分销部崔桥专用仓主仓",
|
||||
"dbWarehouse": "简尚-分销部崔桥专用仓",
|
||||
"wmsCoId": 15404402,
|
||||
"openingQty": 620,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-运营C崔桥工厂仓专用仓主仓",
|
||||
"dbWarehouse": "简尚-运营C崔桥工厂仓专用仓",
|
||||
"wmsCoId": 15301815,
|
||||
"openingQty": 286,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-运营D苏北专用仓主仓",
|
||||
"dbWarehouse": "简尚-运营D苏北专用仓",
|
||||
"wmsCoId": 15406876,
|
||||
"openingQty": 280,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-运营C睢宁专用仓主仓",
|
||||
"dbWarehouse": "简尚-运营C睢宁专用仓",
|
||||
"wmsCoId": 15301813,
|
||||
"openingQty": 193,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-运营C新苏北专用仓主仓",
|
||||
"dbWarehouse": "简尚-运营C新苏北专用仓",
|
||||
"wmsCoId": 15301812,
|
||||
"openingQty": 155,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-运营D睢宁专用仓主仓",
|
||||
"dbWarehouse": "简尚-运营D睢宁专用仓",
|
||||
"wmsCoId": 15051907,
|
||||
"openingQty": 109,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -1,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-山东青岗集京东仓主仓",
|
||||
"dbWarehouse": "简尚-山东青岗集京东仓",
|
||||
"wmsCoId": 15406664,
|
||||
"openingQty": 90,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-运营D专用仓库主仓",
|
||||
"dbWarehouse": "简尚-运营D专用仓库",
|
||||
"wmsCoId": 15051915,
|
||||
"openingQty": 180,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -125,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "简尚-崔桥工厂京东仓主仓",
|
||||
"dbWarehouse": "简尚-崔桥工厂京东仓",
|
||||
"wmsCoId": 15045345,
|
||||
"openingQty": 1,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": 0,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
},
|
||||
{
|
||||
"warehouse": "京仓-横林二仓京东云仓主仓",
|
||||
"dbWarehouse": "京仓-横林二仓京东云仓",
|
||||
"wmsCoId": 15197138,
|
||||
"openingQty": -3082,
|
||||
"openingQtyExtra": 0,
|
||||
"processIn": 0,
|
||||
"purchaseReturn": 0,
|
||||
"purchaseIn": 0,
|
||||
"otherIn": 0,
|
||||
"otherReturn": 0,
|
||||
"salesOut": -48,
|
||||
"stocktaking": 0,
|
||||
"processOut": 0,
|
||||
"transferOut": 0,
|
||||
"salesReturn": 0,
|
||||
"transferIn": 0,
|
||||
"otherOut": 0
|
||||
}
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
DAILY_INVENTORY_LEVEL1,
|
||||
DAILY_INVENTORY_LEVEL2,
|
||||
BASELINE_DATE,
|
||||
FLOW_COLUMNS,
|
||||
DAILY_INVENTORY_BASELINE,
|
||||
};
|
||||
1782
backend/src/index.js
1782
backend/src/index.js
File diff suppressed because it is too large
Load Diff
9
frontend/src/api/dailyInventory.js
Normal file
9
frontend/src/api/dailyInventory.js
Normal file
@ -0,0 +1,9 @@
|
||||
import request from './request';
|
||||
|
||||
const LIST_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
|
||||
// 每日进销存:25 仓 × 进销存流水 + 实时期末 + 分级库存量
|
||||
export const getDailyInventory = () => request.get(
|
||||
'/api/daily-inventory',
|
||||
{ timeout: LIST_TIMEOUT_MS }
|
||||
);
|
||||
@ -1,195 +1,201 @@
|
||||
export const DEFAULT_SIDEBAR_CONFIG = {
|
||||
collapsedByDefault: false,
|
||||
darkModeByDefault: false,
|
||||
entries: [
|
||||
{ key: 'product-monitoring', type: 'item', label: '商品监控', icon: 'appstore', routeKey: 'product-monitoring', visible: true, order: 12 },
|
||||
{ key: 'ops-b-incentive', type: 'item', label: '激励看板', icon: 'line-chart', routeKey: 'ops-b-incentive', visible: true, order: 15 },
|
||||
{ key: 'dashboard-preview', type: 'item', label: '仪表盘', icon: 'dashboard', routeKey: 'dashboard-preview', visible: true, order: 50 },
|
||||
{
|
||||
key: 'marketing-management', type: 'group', label: '营销管理', icon: 'shopping', visible: true, order: 60, defaultExpanded: true,
|
||||
children: [
|
||||
{ key: 'marketing-events', label: '营销活动', icon: 'projection', routeKey: 'marketing-events', visible: true, order: 30 },
|
||||
{ key: 'promotion-module', label: '推广模块', icon: 'line-chart', routeKey: 'promotion-module', visible: true, order: 50 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'product-management', type: 'group', label: '商品管理', icon: 'appstore', visible: true, order: 70, defaultExpanded: true,
|
||||
children: [
|
||||
{ key: 'product-analysis', label: '商品分析', icon: 'table', routeKey: 'product-analysis', visible: true, order: 20 },
|
||||
],
|
||||
},
|
||||
{ key: 'work-orders', type: 'item', label: '工单待办', icon: 'message', routeKey: 'work-orders', visible: true, order: 100 },
|
||||
{
|
||||
key: 'modular-stock-monitor', type: 'group', label: '模块化库存监控', icon: 'appstore', visible: true, order: 105, defaultExpanded: true,
|
||||
children: [
|
||||
{ key: 'modular-stock-monitor-accessory', label: '配件库存监控', icon: 'table', routeKey: 'modular-stock-monitor-accessory', visible: true, order: 10 },
|
||||
{ key: 'mkh-bom-data', label: 'MKH-BOM资料', icon: 'table', routeKey: 'mkh-bom-data', visible: true, order: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'manage', type: 'group', label: '系统管理', icon: 'setting', visible: true, order: 900, defaultExpanded: false, requiresAdmin: true,
|
||||
children: [
|
||||
{ key: 'settings', label: '系统设置', icon: 'setting', routeKey: 'settings', visible: true, order: 10, requiresAdmin: true },
|
||||
{ key: 'online-desktop-users', label: '在线客户端', icon: 'team', routeKey: 'online-desktop-users', visible: true, order: 30, requiresAdmin: true },
|
||||
{ key: 'desktop-execution-tasks', label: '执行任务', icon: 'projection', routeKey: 'desktop-execution-tasks', visible: true, order: 40, requiresAdmin: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const cloneDefaultConfig = () => JSON.parse(JSON.stringify(DEFAULT_SIDEBAR_CONFIG));
|
||||
|
||||
const flattenEntryKeys = (entries = []) => entries.flatMap((entry) => [
|
||||
entry.key,
|
||||
...flattenEntryKeys(entry.children || []),
|
||||
]).filter(Boolean);
|
||||
|
||||
const flattenRouteKeys = (entries = []) => entries.flatMap((entry) => [
|
||||
entry.routeKey,
|
||||
...flattenRouteKeys(entry.children || []),
|
||||
]).filter(Boolean);
|
||||
|
||||
const ALLOWED_ENTRY_KEYS = new Set(flattenEntryKeys(DEFAULT_SIDEBAR_CONFIG.entries));
|
||||
const ALLOWED_ROUTE_KEYS = new Set(flattenRouteKeys(DEFAULT_SIDEBAR_CONFIG.entries));
|
||||
|
||||
// 激励看板原为「运营工具 → 运营中心B激励看板」,现升级为顶层入口;用于迁移存量侧边栏配置
|
||||
const OPS_B_INCENTIVE_LEGACY_LABEL = '运营中心B激励看板';
|
||||
const OPS_B_INCENTIVE_LEGACY_ORDER = 6;
|
||||
const PROMOTED_TOP_LEVEL_ENTRY_KEYS = new Set(['ops-b-incentive']);
|
||||
// 把存量配置里仍嵌在分组下的已升级条目摘出来,提升为顶层配置参与合并
|
||||
const extractPromotedTopLevelEntries = (entries = []) => {
|
||||
const promoted = [];
|
||||
const nextEntries = entries.map((entry) => {
|
||||
if (!Array.isArray(entry?.children)) return entry;
|
||||
const children = [];
|
||||
entry.children.forEach((child) => {
|
||||
if (child?.key && PROMOTED_TOP_LEVEL_ENTRY_KEYS.has(child.key)) {
|
||||
promoted.push(child);
|
||||
} else {
|
||||
children.push(child);
|
||||
}
|
||||
});
|
||||
return children.length === entry.children.length ? entry : { ...entry, children };
|
||||
});
|
||||
return { nextEntries, promoted };
|
||||
};
|
||||
|
||||
const mergeSidebarEntries = (defaultEntries = [], configuredEntries = []) => {
|
||||
const defaultByKey = new Map(defaultEntries.map((entry) => [entry?.key, entry]).filter(([key]) => key));
|
||||
const configuredKeys = new Set();
|
||||
|
||||
const mergeEntry = (defaultEntry = {}, configuredEntry = {}) => {
|
||||
const merged = { ...defaultEntry, ...configuredEntry };
|
||||
if (defaultEntry.type) {
|
||||
merged.type = defaultEntry.type;
|
||||
}
|
||||
if (defaultEntry.routeKey) {
|
||||
merged.routeKey = defaultEntry.routeKey;
|
||||
}
|
||||
if (typeof defaultEntry.requiresAdmin !== 'undefined') {
|
||||
merged.requiresAdmin = defaultEntry.requiresAdmin;
|
||||
}
|
||||
if (
|
||||
defaultEntry.key === 'ops-b-incentive'
|
||||
&& (!configuredEntry.label || configuredEntry.label === OPS_B_INCENTIVE_LEGACY_LABEL)
|
||||
) {
|
||||
merged.label = defaultEntry.label;
|
||||
}
|
||||
if (
|
||||
defaultEntry.key === 'ops-b-incentive'
|
||||
&& (configuredEntry.order == null || configuredEntry.order === OPS_B_INCENTIVE_LEGACY_ORDER)
|
||||
) {
|
||||
merged.order = defaultEntry.order;
|
||||
}
|
||||
const hasDefaultChildren = Array.isArray(defaultEntry.children);
|
||||
const hasConfiguredChildren = Array.isArray(configuredEntry.children);
|
||||
|
||||
if (hasDefaultChildren || hasConfiguredChildren) {
|
||||
if (hasDefaultChildren) {
|
||||
merged.type = defaultEntry.type;
|
||||
} else if (defaultEntry.routeKey) {
|
||||
merged.type = defaultEntry.type || 'item';
|
||||
delete merged.children;
|
||||
delete merged.defaultExpanded;
|
||||
return merged;
|
||||
}
|
||||
merged.children = mergeSidebarEntries(defaultEntry.children || [], configuredEntry.children || []);
|
||||
}
|
||||
|
||||
return merged;
|
||||
};
|
||||
|
||||
const configuredMerged = configuredEntries.map((entry) => {
|
||||
if (entry?.key) {
|
||||
configuredKeys.add(entry.key);
|
||||
}
|
||||
|
||||
const defaultEntry = defaultByKey.get(entry?.key);
|
||||
return defaultEntry ? mergeEntry(defaultEntry, entry) : entry;
|
||||
});
|
||||
|
||||
const missingDefaults = defaultEntries
|
||||
.filter((entry) => entry?.key && !configuredKeys.has(entry.key))
|
||||
.map((entry) => mergeEntry(entry, {}));
|
||||
|
||||
return [...configuredMerged, ...missingDefaults];
|
||||
};
|
||||
|
||||
const mergeSidebarConfigWithDefaults = (config) => {
|
||||
const defaults = cloneDefaultConfig();
|
||||
const nestedEntries = (config?.entries || []);
|
||||
const { nextEntries, promoted } = extractPromotedTopLevelEntries(nestedEntries);
|
||||
const topLevelKeys = new Set(nextEntries.map((entry) => entry?.key).filter(Boolean));
|
||||
const promotedNotDuplicated = promoted.some((entry) => topLevelKeys.has(entry.key)) ? [] : promoted;
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...config,
|
||||
entries: mergeSidebarEntries(defaults.entries, [...nextEntries, ...promotedNotDuplicated]),
|
||||
};
|
||||
};
|
||||
|
||||
const pruneSidebarEntry = (entry) => {
|
||||
if (!entry || !ALLOWED_ENTRY_KEYS.has(entry.key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const children = Array.isArray(entry.children)
|
||||
? entry.children.map(pruneSidebarEntry).filter(Boolean)
|
||||
: undefined;
|
||||
const hasAllowedRoute = entry.routeKey ? ALLOWED_ROUTE_KEYS.has(entry.routeKey) : false;
|
||||
const hasAllowedChildren = Array.isArray(children) && children.length > 0;
|
||||
const keepEntry = hasAllowedRoute || hasAllowedChildren;
|
||||
|
||||
if (!keepEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...entry,
|
||||
...(Array.isArray(entry.children) ? { children } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
const removeRetiredEntries = (config) => ({
|
||||
...config,
|
||||
entries: (config.entries || []).map(pruneSidebarEntry).filter(Boolean),
|
||||
});
|
||||
|
||||
export const parseSidebarConfig = (value) => {
|
||||
if (!value) return cloneDefaultConfig();
|
||||
|
||||
try {
|
||||
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
||||
if (!parsed || !Array.isArray(parsed.entries)) return cloneDefaultConfig();
|
||||
return removeRetiredEntries(mergeSidebarConfigWithDefaults(parsed));
|
||||
} catch {
|
||||
return cloneDefaultConfig();
|
||||
}
|
||||
};
|
||||
|
||||
export const sortSidebarEntries = (entries = []) => (
|
||||
[...entries]
|
||||
.filter((entry) => entry?.visible !== false)
|
||||
.sort((a, b) => Number(a.order || 0) - Number(b.order || 0))
|
||||
);
|
||||
export const DEFAULT_SIDEBAR_CONFIG = {
|
||||
collapsedByDefault: false,
|
||||
darkModeByDefault: false,
|
||||
entries: [
|
||||
{ key: 'product-monitoring', type: 'item', label: '商品监控', icon: 'appstore', routeKey: 'product-monitoring', visible: true, order: 12 },
|
||||
{ key: 'ops-b-incentive', type: 'item', label: '激励看板', icon: 'line-chart', routeKey: 'ops-b-incentive', visible: true, order: 15 },
|
||||
{ key: 'dashboard-preview', type: 'item', label: '仪表盘', icon: 'dashboard', routeKey: 'dashboard-preview', visible: true, order: 50 },
|
||||
{
|
||||
key: 'marketing-management', type: 'group', label: '营销管理', icon: 'shopping', visible: true, order: 60, defaultExpanded: true,
|
||||
children: [
|
||||
{ key: 'marketing-events', label: '营销活动', icon: 'projection', routeKey: 'marketing-events', visible: true, order: 30 },
|
||||
{ key: 'promotion-module', label: '推广模块', icon: 'line-chart', routeKey: 'promotion-module', visible: true, order: 50 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'product-management', type: 'group', label: '商品管理', icon: 'appstore', visible: true, order: 70, defaultExpanded: true,
|
||||
children: [
|
||||
{ key: 'product-analysis', label: '商品分析', icon: 'table', routeKey: 'product-analysis', visible: true, order: 20 },
|
||||
],
|
||||
},
|
||||
{ key: 'work-orders', type: 'item', label: '工单待办', icon: 'message', routeKey: 'work-orders', visible: true, order: 100 },
|
||||
{
|
||||
key: 'modular-stock-monitor', type: 'group', label: '模块化库存监控', icon: 'appstore', visible: true, order: 105, defaultExpanded: true,
|
||||
children: [
|
||||
{ key: 'modular-stock-monitor-accessory', label: '配件库存监控', icon: 'table', routeKey: 'modular-stock-monitor-accessory', visible: true, order: 10 },
|
||||
{ key: 'mkh-bom-data', label: 'MKH-BOM资料', icon: 'table', routeKey: 'mkh-bom-data', visible: true, order: 20 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'large-stock-monitor', type: 'group', label: '大货库存监控', icon: 'appstore', visible: true, order: 200, defaultExpanded: true,
|
||||
children: [
|
||||
{ key: 'daily-inventory', label: '每日进销存', icon: 'table', routeKey: 'daily-inventory', visible: true, order: 10 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'manage', type: 'group', label: '系统管理', icon: 'setting', visible: true, order: 900, defaultExpanded: false, requiresAdmin: true,
|
||||
children: [
|
||||
{ key: 'settings', label: '系统设置', icon: 'setting', routeKey: 'settings', visible: true, order: 10, requiresAdmin: true },
|
||||
{ key: 'online-desktop-users', label: '在线客户端', icon: 'team', routeKey: 'online-desktop-users', visible: true, order: 30, requiresAdmin: true },
|
||||
{ key: 'desktop-execution-tasks', label: '执行任务', icon: 'projection', routeKey: 'desktop-execution-tasks', visible: true, order: 40, requiresAdmin: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const cloneDefaultConfig = () => JSON.parse(JSON.stringify(DEFAULT_SIDEBAR_CONFIG));
|
||||
|
||||
const flattenEntryKeys = (entries = []) => entries.flatMap((entry) => [
|
||||
entry.key,
|
||||
...flattenEntryKeys(entry.children || []),
|
||||
]).filter(Boolean);
|
||||
|
||||
const flattenRouteKeys = (entries = []) => entries.flatMap((entry) => [
|
||||
entry.routeKey,
|
||||
...flattenRouteKeys(entry.children || []),
|
||||
]).filter(Boolean);
|
||||
|
||||
const ALLOWED_ENTRY_KEYS = new Set(flattenEntryKeys(DEFAULT_SIDEBAR_CONFIG.entries));
|
||||
const ALLOWED_ROUTE_KEYS = new Set(flattenRouteKeys(DEFAULT_SIDEBAR_CONFIG.entries));
|
||||
|
||||
// 激励看板原为「运营工具 → 运营中心B激励看板」,现升级为顶层入口;用于迁移存量侧边栏配置
|
||||
const OPS_B_INCENTIVE_LEGACY_LABEL = '运营中心B激励看板';
|
||||
const OPS_B_INCENTIVE_LEGACY_ORDER = 6;
|
||||
const PROMOTED_TOP_LEVEL_ENTRY_KEYS = new Set(['ops-b-incentive']);
|
||||
// 把存量配置里仍嵌在分组下的已升级条目摘出来,提升为顶层配置参与合并
|
||||
const extractPromotedTopLevelEntries = (entries = []) => {
|
||||
const promoted = [];
|
||||
const nextEntries = entries.map((entry) => {
|
||||
if (!Array.isArray(entry?.children)) return entry;
|
||||
const children = [];
|
||||
entry.children.forEach((child) => {
|
||||
if (child?.key && PROMOTED_TOP_LEVEL_ENTRY_KEYS.has(child.key)) {
|
||||
promoted.push(child);
|
||||
} else {
|
||||
children.push(child);
|
||||
}
|
||||
});
|
||||
return children.length === entry.children.length ? entry : { ...entry, children };
|
||||
});
|
||||
return { nextEntries, promoted };
|
||||
};
|
||||
|
||||
const mergeSidebarEntries = (defaultEntries = [], configuredEntries = []) => {
|
||||
const defaultByKey = new Map(defaultEntries.map((entry) => [entry?.key, entry]).filter(([key]) => key));
|
||||
const configuredKeys = new Set();
|
||||
|
||||
const mergeEntry = (defaultEntry = {}, configuredEntry = {}) => {
|
||||
const merged = { ...defaultEntry, ...configuredEntry };
|
||||
if (defaultEntry.type) {
|
||||
merged.type = defaultEntry.type;
|
||||
}
|
||||
if (defaultEntry.routeKey) {
|
||||
merged.routeKey = defaultEntry.routeKey;
|
||||
}
|
||||
if (typeof defaultEntry.requiresAdmin !== 'undefined') {
|
||||
merged.requiresAdmin = defaultEntry.requiresAdmin;
|
||||
}
|
||||
if (
|
||||
defaultEntry.key === 'ops-b-incentive'
|
||||
&& (!configuredEntry.label || configuredEntry.label === OPS_B_INCENTIVE_LEGACY_LABEL)
|
||||
) {
|
||||
merged.label = defaultEntry.label;
|
||||
}
|
||||
if (
|
||||
defaultEntry.key === 'ops-b-incentive'
|
||||
&& (configuredEntry.order == null || configuredEntry.order === OPS_B_INCENTIVE_LEGACY_ORDER)
|
||||
) {
|
||||
merged.order = defaultEntry.order;
|
||||
}
|
||||
const hasDefaultChildren = Array.isArray(defaultEntry.children);
|
||||
const hasConfiguredChildren = Array.isArray(configuredEntry.children);
|
||||
|
||||
if (hasDefaultChildren || hasConfiguredChildren) {
|
||||
if (hasDefaultChildren) {
|
||||
merged.type = defaultEntry.type;
|
||||
} else if (defaultEntry.routeKey) {
|
||||
merged.type = defaultEntry.type || 'item';
|
||||
delete merged.children;
|
||||
delete merged.defaultExpanded;
|
||||
return merged;
|
||||
}
|
||||
merged.children = mergeSidebarEntries(defaultEntry.children || [], configuredEntry.children || []);
|
||||
}
|
||||
|
||||
return merged;
|
||||
};
|
||||
|
||||
const configuredMerged = configuredEntries.map((entry) => {
|
||||
if (entry?.key) {
|
||||
configuredKeys.add(entry.key);
|
||||
}
|
||||
|
||||
const defaultEntry = defaultByKey.get(entry?.key);
|
||||
return defaultEntry ? mergeEntry(defaultEntry, entry) : entry;
|
||||
});
|
||||
|
||||
const missingDefaults = defaultEntries
|
||||
.filter((entry) => entry?.key && !configuredKeys.has(entry.key))
|
||||
.map((entry) => mergeEntry(entry, {}));
|
||||
|
||||
return [...configuredMerged, ...missingDefaults];
|
||||
};
|
||||
|
||||
const mergeSidebarConfigWithDefaults = (config) => {
|
||||
const defaults = cloneDefaultConfig();
|
||||
const nestedEntries = (config?.entries || []);
|
||||
const { nextEntries, promoted } = extractPromotedTopLevelEntries(nestedEntries);
|
||||
const topLevelKeys = new Set(nextEntries.map((entry) => entry?.key).filter(Boolean));
|
||||
const promotedNotDuplicated = promoted.some((entry) => topLevelKeys.has(entry.key)) ? [] : promoted;
|
||||
|
||||
return {
|
||||
...defaults,
|
||||
...config,
|
||||
entries: mergeSidebarEntries(defaults.entries, [...nextEntries, ...promotedNotDuplicated]),
|
||||
};
|
||||
};
|
||||
|
||||
const pruneSidebarEntry = (entry) => {
|
||||
if (!entry || !ALLOWED_ENTRY_KEYS.has(entry.key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const children = Array.isArray(entry.children)
|
||||
? entry.children.map(pruneSidebarEntry).filter(Boolean)
|
||||
: undefined;
|
||||
const hasAllowedRoute = entry.routeKey ? ALLOWED_ROUTE_KEYS.has(entry.routeKey) : false;
|
||||
const hasAllowedChildren = Array.isArray(children) && children.length > 0;
|
||||
const keepEntry = hasAllowedRoute || hasAllowedChildren;
|
||||
|
||||
if (!keepEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...entry,
|
||||
...(Array.isArray(entry.children) ? { children } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
const removeRetiredEntries = (config) => ({
|
||||
...config,
|
||||
entries: (config.entries || []).map(pruneSidebarEntry).filter(Boolean),
|
||||
});
|
||||
|
||||
export const parseSidebarConfig = (value) => {
|
||||
if (!value) return cloneDefaultConfig();
|
||||
|
||||
try {
|
||||
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
||||
if (!parsed || !Array.isArray(parsed.entries)) return cloneDefaultConfig();
|
||||
return removeRetiredEntries(mergeSidebarConfigWithDefaults(parsed));
|
||||
} catch {
|
||||
return cloneDefaultConfig();
|
||||
}
|
||||
};
|
||||
|
||||
export const sortSidebarEntries = (entries = []) => (
|
||||
[...entries]
|
||||
.filter((entry) => entry?.visible !== false)
|
||||
.sort((a, b) => Number(a.order || 0) - Number(b.order || 0))
|
||||
);
|
||||
|
||||
61
frontend/src/constants/dailyInventory.js
Normal file
61
frontend/src/constants/dailyInventory.js
Normal file
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* 每日进销存 —— 列定义
|
||||
*
|
||||
* 表头取自桌面《库存监控.xlsx》,顺序与表格一致。
|
||||
* source 标记该列数据来源,便于在页面上如实标注口径:
|
||||
* - baseline:22 号基准值(业务库无对应单据表,无法还原)
|
||||
* - runtime:运行时从业务库实时取数
|
||||
*/
|
||||
|
||||
export const COLUMN_SOURCE = {
|
||||
baseline: 'baseline',
|
||||
runtime: 'runtime',
|
||||
};
|
||||
|
||||
/** 表格存在两个名为「期初」的列,第二个加后缀区分 */
|
||||
export const DAILY_INVENTORY_COLUMNS = [
|
||||
{ key: 'warehouse', label: '仓库', source: 'baseline', tip: '取自主数据表 rpt.jst_warehouseInfo 的仓库名,表格显示为「XX主仓」' },
|
||||
{ key: 'openingQty', label: '期初数量', source: 'baseline', tip: '22 号进销存基准值。业务库无按天的历史库存结存表,无法还原,取自 9-22 导出数据' },
|
||||
{ key: 'processIn', label: '加工进仓', source: 'baseline', tip: '22 号基准值。业务库无加工单据表' },
|
||||
{ key: 'purchaseReturn', label: '采购退货', source: 'baseline', tip: '22 号基准值(业务库 jst_purchaseout 虽有单据,但数值与该进销存口径不一致)' },
|
||||
{ key: 'purchaseIn', label: '采购进仓', source: 'baseline', tip: '22 号基准值(同上,业务库数值与进销存口径不一致)' },
|
||||
{ key: 'otherIn', label: '其它进仓', source: 'baseline', tip: '22 号基准值。业务库无对应单据表' },
|
||||
{ key: 'otherReturn', label: '其它退货', source: 'baseline', tip: '22 号基准值。业务库无对应单据表' },
|
||||
{ key: 'salesOut', label: '销售出仓', source: 'baseline', tip: '22 号基准值(负数为出库)。业务库 jst_salesOutboundInfo 数值与该进销存口径不一致' },
|
||||
{ key: 'stocktaking', label: '盘点', source: 'baseline', tip: '22 号基准值。业务库无盘点单据表' },
|
||||
{ key: 'processOut', label: '加工出仓', source: 'baseline', tip: '22 号基准值。业务库无加工单据表' },
|
||||
{ key: 'transferOut', label: '调拨出', source: 'baseline', tip: '22 号基准值(负数为调出)。业务库无调拨单据表' },
|
||||
{ key: 'salesReturn', label: '销售退货', source: 'baseline', tip: '22 号基准值(业务库 jst_returnAndRefundInfo 数值与该进销存口径不一致)' },
|
||||
{ key: 'transferIn', label: '调拨入', source: 'baseline', tip: '22 号基准值。业务库无调拨单据表' },
|
||||
{ key: 'openingQtyExtra', label: '期初(2)', source: 'baseline', tip: '表格中的第二个「期初」列(9-22 导出值)' },
|
||||
{ key: 'otherOut', label: '其它出库', source: 'baseline', tip: '22 号基准值(负数为出库)。业务库无对应单据表' },
|
||||
{ key: 'closingQty', label: '期末数量', source: 'runtime', tip: '聚水潭实时库存 rpt.jst_inventoryInfo,按仓库 wms_co_id 单仓统计(与表格 22 号期末一致,24/25 仓精确匹配)' },
|
||||
{ key: 'turnoverDays', label: '库存周转天数', source: 'runtime', tip: '月度发货总件数 / 月度日均库存件数 × 100%(全员均值),数据来自 pm_stockMonitor' },
|
||||
{ key: 'warnLevel', label: '滞销预警级库存量', source: 'runtime', tip: '库龄 < 3 个月 且 30% ≤ 周转率 ≤ 199% 的 SKU 库存合计' },
|
||||
{ key: 'slowLevel', label: '商品慢流级库存量', source: 'runtime', tip: '库龄 < 3 个月且周转率 ≥ 200%,或库龄 ≥ 3 个月且周转率 30%~99% 的 SKU 库存合计' },
|
||||
{ key: 'staleLevel', label: '商品呆滞级库存量', source: 'runtime', tip: '库龄 3~12 个月且周转率 ≥ 100%,或库龄 > 12 个月且周转率 100%~199% 的 SKU 库存合计' },
|
||||
{ key: 'zombieLevel', label: '商品僵尸级库存量', source: 'runtime', tip: '库龄 > 12 个月 且 周转率 ≥ 200% 的 SKU 库存合计' },
|
||||
];
|
||||
|
||||
/** 口径说明弹框文案 */
|
||||
export const FORMULA_DESCRIPTION = [
|
||||
'数据来源说明:',
|
||||
'· 期初数量 / 加工进仓 / 采购退货 / 采购进仓 / 其它进仓 / 其它退货 / 销售出仓 / 盘点 / 加工出仓 / 调拨出 / 销售退货 / 调拨入 / 其它出库:',
|
||||
' 取自 2026-09-22 聚水潭进销存导出值。业务库 JSReportReplica 无历史库存结存表,',
|
||||
' 也缺少调拨 / 盘点 / 加工 / 其它出入库单据表;已有的采购与销售单据数值与该进销存口径不一致,故采用导出值。',
|
||||
'· 期末数量:运行时取聚水潭实时库存 rpt.jst_inventoryInfo(按 wms_co_id 单仓),随刷新滚动。',
|
||||
'· 库存周转天数 / 四级库存量:运行时由库存快照 rpt.pm_stockMonitor 计算,基准时刻为快照日次日 01:00。',
|
||||
'',
|
||||
'算式:',
|
||||
'· 期末数量 = 期初数量 + 加工进仓 + 采购进仓 + 其它进仓 + 其它退货 + 销售退货 + 调拨入',
|
||||
' − 采购退货 − 销售出仓 − 盘点 − 加工出仓 − 调拨出 − 其它出库',
|
||||
'· 库存周转天数 = 月度发货总件数 / 月度日均库存件数 × 100%',
|
||||
' 其中 月度发货总件数 = threeAvgSale × 30,月度日均库存件数 = jsStock',
|
||||
'· 库存分级(库龄 + 周转率双维度):',
|
||||
' 库龄 < 3 个月 且 周转率 30%~199% → 滞销预警',
|
||||
' 库龄 < 3 个月 且 周转率 ≥ 200% → 商品慢流',
|
||||
' 库龄 ≥ 3 个月 且 周转率 30%~99% → 商品慢流',
|
||||
' 库龄 3~12 个月 且 周转率 ≥ 100% → 商品呆滞',
|
||||
' 库龄 > 12 个月 且 周转率 100%~199% → 商品呆滞',
|
||||
' 库龄 > 12 个月 且 周转率 ≥ 200% → 商品僵尸',
|
||||
];
|
||||
@ -1,12 +1,13 @@
|
||||
const DEFAULT_PLANNING_ROLES = { admin: true, analyst: true, user: true, guest: false };
|
||||
const DEFAULT_SYSTEM_ROLES = { admin: true, analyst: false, user: false, guest: false };
|
||||
|
||||
export const SIDEBAR_PAGES = [
|
||||
{ pageKey: 'dashboard_preview', path: '/planning/dev-style-preview', name: '仪表盘', category: 'planning', defaultRoles: DEFAULT_PLANNING_ROLES },
|
||||
{ pageKey: 'settings', path: '/settings', name: '系统设置', category: 'system', defaultRoles: DEFAULT_SYSTEM_ROLES },
|
||||
];
|
||||
|
||||
export const SIDEBAR_PAGE_MAP = SIDEBAR_PAGES.reduce((acc, item) => {
|
||||
acc[item.pageKey] = item;
|
||||
return acc;
|
||||
}, {});
|
||||
const DEFAULT_PLANNING_ROLES = { admin: true, analyst: true, user: true, guest: false };
|
||||
const DEFAULT_SYSTEM_ROLES = { admin: true, analyst: false, user: false, guest: false };
|
||||
|
||||
export const SIDEBAR_PAGES = [
|
||||
{ pageKey: 'dashboard_preview', path: '/planning/dev-style-preview', name: '仪表盘', category: 'planning', defaultRoles: DEFAULT_PLANNING_ROLES },
|
||||
{ pageKey: 'settings', path: '/settings', name: '系统设置', category: 'system', defaultRoles: DEFAULT_SYSTEM_ROLES },
|
||||
{ pageKey: 'daily_inventory', path: '/planning/operation/daily-inventory', name: '每日进销存', category: 'planning', defaultRoles: DEFAULT_PLANNING_ROLES },
|
||||
];
|
||||
|
||||
export const SIDEBAR_PAGE_MAP = SIDEBAR_PAGES.reduce((acc, item) => {
|
||||
acc[item.pageKey] = item;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
177
frontend/src/pages/DailyInventory/index.jsx
Normal file
177
frontend/src/pages/DailyInventory/index.jsx
Normal file
@ -0,0 +1,177 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Button, Modal, Space, Spin, Table, Tooltip, Typography } from 'antd';
|
||||
import { InfoCircleOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { SycmPage } from '../../components/Common';
|
||||
import { getDailyInventory } from '../../api/dailyInventory';
|
||||
import {
|
||||
DAILY_INVENTORY_COLUMNS,
|
||||
COLUMN_SOURCE,
|
||||
FORMULA_DESCRIPTION,
|
||||
} from '../../constants/dailyInventory.js';
|
||||
import './style.css';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const TipTitle = ({ tip, children }) => (
|
||||
<Tooltip title={tip}>
|
||||
<span className="daily-inventory-th-tip">{children}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const int = (value) => {
|
||||
if (value === null || value === undefined || value === '') return '—';
|
||||
return Number(value).toLocaleString('zh-CN', { maximumFractionDigits: 0 });
|
||||
};
|
||||
|
||||
const decimal = (value, digits = 1) => {
|
||||
if (value === null || value === undefined || value === '') return '—';
|
||||
return Number(value).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
});
|
||||
};
|
||||
|
||||
/** 负数标红、正数标黑:出入库列以负数表示流出,便于一眼分辨方向 */
|
||||
const renderSigned = (value) => {
|
||||
if (value === null || value === undefined || value === '') return '—';
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n) || n === 0) return <span className="daily-inventory-cell--zero">0</span>;
|
||||
return <span className={n < 0 ? 'daily-inventory-cell--out' : 'daily-inventory-cell--in'}>{int(n)}</span>;
|
||||
};
|
||||
|
||||
const DailyInventory = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [data, setData] = useState(null);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await getDailyInventory();
|
||||
setData(res?.data ?? null);
|
||||
} catch (err) {
|
||||
setError(err?.response?.data?.error || err?.message || '每日进销存数据加载失败');
|
||||
setData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const items = useMemo(() => data?.items ?? [], [data]);
|
||||
|
||||
const columns = useMemo(() => DAILY_INVENTORY_COLUMNS.map((col) => {
|
||||
const isRuntime = col.source === COLUMN_SOURCE.runtime;
|
||||
return {
|
||||
title: <TipTitle tip={col.tip}>{col.label}</TipTitle>,
|
||||
dataIndex: col.key,
|
||||
key: col.key,
|
||||
align: col.key === 'warehouse' ? 'left' : 'right',
|
||||
width: col.key === 'warehouse' ? 240 : 130,
|
||||
fixed: col.key === 'warehouse' ? 'left' : undefined,
|
||||
className: isRuntime ? 'daily-inventory-col--runtime' : undefined,
|
||||
render: (value, record) => {
|
||||
if (record?.isTotal) {
|
||||
// 合计行:周转天数不累加,显示「—」
|
||||
if (col.key === 'turnoverDays') return '—';
|
||||
return int(value);
|
||||
}
|
||||
if (col.key === 'warehouse') return <span className="daily-inventory-warehouse">{value}</span>;
|
||||
if (col.key === 'closingQty') return int(value);
|
||||
if (col.key === 'turnoverDays') return decimal(value, 1);
|
||||
return renderSigned(value);
|
||||
},
|
||||
};
|
||||
}), []);
|
||||
|
||||
const tableRows = useMemo(() => {
|
||||
if (!data?.totals) return items;
|
||||
return [...items, { ...data.totals, key: '__total__' }];
|
||||
}, [items, data]);
|
||||
|
||||
const noGradeCount = useMemo(() => items.filter((it) => !it.hasGrade).length, [items]);
|
||||
|
||||
return (
|
||||
<SycmPage className="daily-inventory-page">
|
||||
<div className="daily-inventory-header">
|
||||
<div className="daily-inventory-header__title">
|
||||
<Title level={4} style={{ margin: 0 }}>每日进销存</Title>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<InfoCircleOutlined />}
|
||||
onClick={() => setHelpOpen(true)}
|
||||
className="daily-inventory-header__help"
|
||||
>
|
||||
口径说明
|
||||
</Button>
|
||||
</div>
|
||||
<div className="daily-inventory-header__meta">
|
||||
<span>基准日期:{data?.baselineDate || '—'}</span>
|
||||
<span>库存快照日:{data?.latestBizDate || '—'}</span>
|
||||
<span>分级基准时刻:{data?.asOf || '—'}</span>
|
||||
{data?.items?.[0]?.closingTakenAt ? (
|
||||
<span>
|
||||
库存取数时间:{new Date(data.items[0].closingTakenAt).toLocaleString('zh-CN', { hour12: false })}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="数据来源混合,请注意口径"
|
||||
description={
|
||||
<span>
|
||||
期初数量与 12 个流水列取自 <b>{data?.baselineDate || '2026-09-22'}</b> 聚水潭进销存导出值(业务库缺少调拨/盘点/加工/其它出入库单据表,无法还原);
|
||||
期末数量与右侧 5 列为运行时实时取数。悬浮表头可看每列口径。
|
||||
{noGradeCount > 0 ? ` 其中 ${noGradeCount} 个仓在库存快照中无数据,周转天数与四级库存量显示「—」。` : ''}
|
||||
</span>
|
||||
}
|
||||
style={{ marginBottom: 12 }}
|
||||
/>
|
||||
|
||||
{error ? <Alert type="error" showIcon message={error} style={{ marginBottom: 12 }} /> : null}
|
||||
|
||||
<Spin spinning={loading}>
|
||||
<Table
|
||||
className="daily-inventory-table"
|
||||
columns={columns}
|
||||
dataSource={tableRows}
|
||||
rowKey={(record) => record.key || record.warehouse}
|
||||
size="small"
|
||||
bordered
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content', y: 'calc(100vh - 300px)' }}
|
||||
rowClassName={(record) => (record?.isTotal ? 'daily-inventory-row--total' : '')}
|
||||
/>
|
||||
</Spin>
|
||||
|
||||
<Text type="secondary" className="daily-inventory-footer">
|
||||
共 {items.length} 个仓库;蓝色底纹列为运行时取数,其余为基准值。
|
||||
</Text>
|
||||
|
||||
<Modal
|
||||
title="计算口径说明"
|
||||
open={helpOpen}
|
||||
onCancel={() => setHelpOpen(false)}
|
||||
footer={null}
|
||||
width={720}
|
||||
>
|
||||
<div className="daily-inventory-help">
|
||||
{FORMULA_DESCRIPTION.map((line, i) => (
|
||||
<p key={i} className={line.startsWith(' ') ? 'is-indent' : ''}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</SycmPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default DailyInventory;
|
||||
91
frontend/src/pages/DailyInventory/style.css
Normal file
91
frontend/src/pages/DailyInventory/style.css
Normal file
@ -0,0 +1,91 @@
|
||||
.daily-inventory-page {
|
||||
padding: 16px 20px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.daily-inventory-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.daily-inventory-header__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.daily-inventory-header__help {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.daily-inventory-header__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.daily-inventory-th-tip {
|
||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.35);
|
||||
cursor: help;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.daily-inventory-table .ant-table-thead > tr > th {
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.daily-inventory-table .ant-table-tbody > tr > td {
|
||||
padding: 5px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* 运行时取数的列:加淡蓝底纹,与基准值区分 */
|
||||
.daily-inventory-table .daily-inventory-col--runtime {
|
||||
background: rgba(22, 119, 255, 0.04);
|
||||
}
|
||||
|
||||
.daily-inventory-warehouse {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.daily-inventory-cell--out {
|
||||
color: #cf1322;
|
||||
}
|
||||
|
||||
.daily-inventory-cell--in {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.daily-inventory-cell--zero {
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.daily-inventory-row--total > td {
|
||||
font-weight: 600;
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.daily-inventory-footer {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.daily-inventory-help p {
|
||||
margin: 0 0 6px;
|
||||
line-height: 1.7;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.daily-inventory-help p.is-indent {
|
||||
padding-left: 16px;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
@ -1,154 +1,159 @@
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||
import BasicLayout from '../layouts/BasicLayout';
|
||||
import { AuthPageLoadingFallback, PageLoadingFallback } from '../components/AppLoadingSkeleton';
|
||||
|
||||
const Login = lazy(() => import('../pages/Login'));
|
||||
const Install = lazy(() => import('../pages/Install'));
|
||||
const Setup = lazy(() => import('../pages/Setup'));
|
||||
const Settings = lazy(() => import('../pages/Settings'));
|
||||
const OnlineDesktopUsers = lazy(() => import('../pages/OnlineDesktopUsers'));
|
||||
const DesktopExecutionTasks = lazy(() => import('../pages/DesktopExecutionTasks'));
|
||||
const MarketingEvents = lazy(() => import('../pages/MarketingEvents'));
|
||||
const ProductInformation = lazy(() => import('../pages/ProductInformation'));
|
||||
const PromotionModule = lazy(() => import('../pages/PromotionModule'));
|
||||
const OperationPlaceholder = lazy(() => import('../pages/OperationPlaceholder'));
|
||||
const DevelopmentStylePreview = lazy(() => import('../pages/DevelopmentStylePreview'));
|
||||
const ProductAnalysis = lazy(() => import('../pages/ProductAnalysis'));
|
||||
const OpsBIncentiveBoard = lazy(() => import('../pages/PlanningUpload/OpsBIncentiveBoard'));
|
||||
const ProductMonitoring = lazy(() => import('../pages/ProductMonitoring'));
|
||||
const ModularStockMonitor = lazy(() => import('../pages/ModularStockMonitor'));
|
||||
const MkhBomData = lazy(() => import('../pages/MkhBomData'));
|
||||
|
||||
const lazyLoadingElement = <PageLoadingFallback />;
|
||||
|
||||
const renderLazyComponent = (lazyView, props = {}, fallback = lazyLoadingElement) => {
|
||||
const LazyView = lazyView;
|
||||
|
||||
return (
|
||||
<Suspense fallback={fallback}>
|
||||
<LazyView {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const HOME_URL = '/planning/dev-style-preview';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/login',
|
||||
element: renderLazyComponent(Login, {}, <AuthPageLoadingFallback />),
|
||||
},
|
||||
{
|
||||
path: '/callback',
|
||||
element: renderLazyComponent(Login, {}, <AuthPageLoadingFallback />),
|
||||
},
|
||||
{
|
||||
path: '/setup',
|
||||
element: renderLazyComponent(Setup),
|
||||
},
|
||||
{
|
||||
path: '/install',
|
||||
element: renderLazyComponent(Install),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <BasicLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'channel',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/marketing-events',
|
||||
element: <Navigate to="/planning/operation/marketing-events/activity-products" replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/marketing-events/activity-products',
|
||||
element: renderLazyComponent(MarketingEvents),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/marketing-tools',
|
||||
element: <Navigate to="/planning/operation/marketing-events" replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/product-information',
|
||||
element: renderLazyComponent(ProductInformation),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/product-analysis',
|
||||
element: renderLazyComponent(ProductAnalysis),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/promotion-module',
|
||||
element: renderLazyComponent(PromotionModule),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/ops-b-incentive',
|
||||
element: renderLazyComponent(OpsBIncentiveBoard),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/product-monitoring',
|
||||
element: renderLazyComponent(ProductMonitoring),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/modular-stock-monitor',
|
||||
element: renderLazyComponent(ModularStockMonitor),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/mkh-bom-data',
|
||||
element: renderLazyComponent(MkhBomData),
|
||||
},
|
||||
{ path: 'planning/operation/store-checkup', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/review-analysis', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/customer-service-checkup', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/pricing-calculator', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/desktop-client-download', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/ai-buyer-show', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/ai-seeding-video', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'settings/desktop-client-releases', element: <Navigate to={HOME_URL} replace /> },
|
||||
{
|
||||
path: 'planning/operation/:featureKey',
|
||||
element: renderLazyComponent(OperationPlaceholder),
|
||||
},
|
||||
{
|
||||
path: 'planning/upload',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning/dev-style-preview',
|
||||
element: renderLazyComponent(DevelopmentStylePreview),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
element: renderLazyComponent(Settings),
|
||||
},
|
||||
{
|
||||
path: 'settings/online-desktop-users',
|
||||
element: renderLazyComponent(OnlineDesktopUsers),
|
||||
},
|
||||
{
|
||||
path: 'settings/desktop-execution-tasks',
|
||||
element: renderLazyComponent(DesktopExecutionTasks),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export default router;
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { createBrowserRouter, Navigate } from 'react-router-dom';
|
||||
import BasicLayout from '../layouts/BasicLayout';
|
||||
import { AuthPageLoadingFallback, PageLoadingFallback } from '../components/AppLoadingSkeleton';
|
||||
|
||||
const Login = lazy(() => import('../pages/Login'));
|
||||
const Install = lazy(() => import('../pages/Install'));
|
||||
const Setup = lazy(() => import('../pages/Setup'));
|
||||
const Settings = lazy(() => import('../pages/Settings'));
|
||||
const OnlineDesktopUsers = lazy(() => import('../pages/OnlineDesktopUsers'));
|
||||
const DesktopExecutionTasks = lazy(() => import('../pages/DesktopExecutionTasks'));
|
||||
const MarketingEvents = lazy(() => import('../pages/MarketingEvents'));
|
||||
const ProductInformation = lazy(() => import('../pages/ProductInformation'));
|
||||
const PromotionModule = lazy(() => import('../pages/PromotionModule'));
|
||||
const OperationPlaceholder = lazy(() => import('../pages/OperationPlaceholder'));
|
||||
const DevelopmentStylePreview = lazy(() => import('../pages/DevelopmentStylePreview'));
|
||||
const ProductAnalysis = lazy(() => import('../pages/ProductAnalysis'));
|
||||
const OpsBIncentiveBoard = lazy(() => import('../pages/PlanningUpload/OpsBIncentiveBoard'));
|
||||
const ProductMonitoring = lazy(() => import('../pages/ProductMonitoring'));
|
||||
const ModularStockMonitor = lazy(() => import('../pages/ModularStockMonitor'));
|
||||
const MkhBomData = lazy(() => import('../pages/MkhBomData'));
|
||||
const DailyInventory = lazy(() => import('../pages/DailyInventory'));
|
||||
|
||||
const lazyLoadingElement = <PageLoadingFallback />;
|
||||
|
||||
const renderLazyComponent = (lazyView, props = {}, fallback = lazyLoadingElement) => {
|
||||
const LazyView = lazyView;
|
||||
|
||||
return (
|
||||
<Suspense fallback={fallback}>
|
||||
<LazyView {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
const HOME_URL = '/planning/dev-style-preview';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/login',
|
||||
element: renderLazyComponent(Login, {}, <AuthPageLoadingFallback />),
|
||||
},
|
||||
{
|
||||
path: '/callback',
|
||||
element: renderLazyComponent(Login, {}, <AuthPageLoadingFallback />),
|
||||
},
|
||||
{
|
||||
path: '/setup',
|
||||
element: renderLazyComponent(Setup),
|
||||
},
|
||||
{
|
||||
path: '/install',
|
||||
element: renderLazyComponent(Install),
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
element: <BasicLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'channel',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/marketing-events',
|
||||
element: <Navigate to="/planning/operation/marketing-events/activity-products" replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/marketing-events/activity-products',
|
||||
element: renderLazyComponent(MarketingEvents),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/marketing-tools',
|
||||
element: <Navigate to="/planning/operation/marketing-events" replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/product-information',
|
||||
element: renderLazyComponent(ProductInformation),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/product-analysis',
|
||||
element: renderLazyComponent(ProductAnalysis),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/promotion-module',
|
||||
element: renderLazyComponent(PromotionModule),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/ops-b-incentive',
|
||||
element: renderLazyComponent(OpsBIncentiveBoard),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/product-monitoring',
|
||||
element: renderLazyComponent(ProductMonitoring),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/modular-stock-monitor',
|
||||
element: renderLazyComponent(ModularStockMonitor),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/mkh-bom-data',
|
||||
element: renderLazyComponent(MkhBomData),
|
||||
},
|
||||
{
|
||||
path: 'planning/operation/daily-inventory',
|
||||
element: renderLazyComponent(DailyInventory),
|
||||
},
|
||||
{ path: 'planning/operation/store-checkup', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/review-analysis', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/customer-service-checkup', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/pricing-calculator', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/desktop-client-download', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/ai-buyer-show', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'planning/operation/ai-seeding-video', element: <Navigate to={HOME_URL} replace /> },
|
||||
{ path: 'settings/desktop-client-releases', element: <Navigate to={HOME_URL} replace /> },
|
||||
{
|
||||
path: 'planning/operation/:featureKey',
|
||||
element: renderLazyComponent(OperationPlaceholder),
|
||||
},
|
||||
{
|
||||
path: 'planning/upload',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
{
|
||||
path: 'planning/dev-style-preview',
|
||||
element: renderLazyComponent(DevelopmentStylePreview),
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
element: renderLazyComponent(Settings),
|
||||
},
|
||||
{
|
||||
path: 'settings/online-desktop-users',
|
||||
element: renderLazyComponent(OnlineDesktopUsers),
|
||||
},
|
||||
{
|
||||
path: 'settings/desktop-execution-tasks',
|
||||
element: renderLazyComponent(DesktopExecutionTasks),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to={HOME_URL} replace />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
export default router;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user