feat(consignment): 寄售仓库增加入库时间筛选
This commit is contained in:
@@ -54,6 +54,28 @@ test('寄卖仓库搜索必须覆盖供货打手姓名、打手电话与页面
|
||||
)
|
||||
})
|
||||
|
||||
test('寄卖仓库按通过审核入库时间筛选,结束日期必须包含当天全天', async () => {
|
||||
const source = await readFile(new URL('./consignment-repo.ts', import.meta.url), 'utf8')
|
||||
const inventoryListStart = source.indexOf('export async function listConsignmentInventories')
|
||||
const inventoryListBody = source.slice(
|
||||
inventoryListStart,
|
||||
source.indexOf('\n}\n', inventoryListStart),
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
inventoryListBody.includes(
|
||||
"ci.inbound_at >= ($${values.length}::date::timestamp AT TIME ZONE 'Asia/Shanghai')",
|
||||
),
|
||||
'开始日期必须按上海时区当天零点筛选 inbound_at',
|
||||
)
|
||||
assert.ok(
|
||||
inventoryListBody.includes(
|
||||
"ci.inbound_at < (($${values.length}::date + INTERVAL '1 day')::timestamp AT TIME ZONE 'Asia/Shanghai')",
|
||||
),
|
||||
'结束日期必须筛选到次日零点之前,不能漏掉结束日当天',
|
||||
)
|
||||
})
|
||||
|
||||
test('匹配候选要带出寄卖单号,并且 join 之后所有列名都必须加 ci. 前缀', async () => {
|
||||
const source = await readFile(new URL('./consignment-repo.ts', import.meta.url), 'utf8')
|
||||
const start = source.indexOf('export async function listActiveConsignmentInventoriesForMatching')
|
||||
|
||||
@@ -128,6 +128,8 @@ export async function listConsignmentInventories(filters: {
|
||||
allowMatching?: boolean | undefined
|
||||
workerId?: number | undefined
|
||||
keyword?: string | undefined
|
||||
dateFrom?: string | undefined
|
||||
dateTo?: string | undefined
|
||||
page?: number | undefined
|
||||
pageSize?: number | undefined
|
||||
}): Promise<{ items: ConsignmentInventoryRow[]; total: number }> {
|
||||
@@ -201,6 +203,18 @@ export async function listConsignmentInventories(filters: {
|
||||
OR ccs.name ILIKE $${values.length})`,
|
||||
)
|
||||
}
|
||||
if (filters.dateFrom) {
|
||||
values.push(filters.dateFrom)
|
||||
conditions.push(
|
||||
`ci.inbound_at >= ($${values.length}::date::timestamp AT TIME ZONE 'Asia/Shanghai')`,
|
||||
)
|
||||
}
|
||||
if (filters.dateTo) {
|
||||
values.push(filters.dateTo)
|
||||
conditions.push(
|
||||
`ci.inbound_at < (($${values.length}::date + INTERVAL '1 day')::timestamp AT TIME ZONE 'Asia/Shanghai')`,
|
||||
)
|
||||
}
|
||||
|
||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
||||
// 计数与列表必须使用同一组 join,否则新增的提报单 / 供货打手关键字会直接报错。
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { normalizeInventoryDate } from './consignment-admin-service.js'
|
||||
|
||||
test('寄售仓库入库日期筛选只接受合法的 YYYY-MM-DD', () => {
|
||||
assert.equal(normalizeInventoryDate('2026-11-05'), '2026-11-05')
|
||||
assert.equal(normalizeInventoryDate(' 2026-11-05 '), '2026-11-05')
|
||||
assert.equal(normalizeInventoryDate('2026-02-31'), undefined)
|
||||
assert.equal(normalizeInventoryDate('2026/11/05'), undefined)
|
||||
assert.equal(normalizeInventoryDate(''), undefined)
|
||||
})
|
||||
@@ -294,6 +294,8 @@ export async function fetchAdminConsignmentInventories(query: JsonObject = {}) {
|
||||
const allowMatching =
|
||||
query.allowMatching !== undefined ? String(query.allowMatching) === 'true' : undefined
|
||||
const keyword = query.keyword ? String(query.keyword).trim() : undefined
|
||||
const dateFrom = normalizeInventoryDate(query.dateFrom)
|
||||
const dateTo = normalizeInventoryDate(query.dateTo)
|
||||
const page = query.page ? Number(query.page) : 1
|
||||
const pageSize = query.pageSize ? Number(query.pageSize) : 20
|
||||
|
||||
@@ -309,6 +311,8 @@ export async function fetchAdminConsignmentInventories(query: JsonObject = {}) {
|
||||
diamondLevel,
|
||||
allowMatching,
|
||||
keyword,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
page,
|
||||
pageSize,
|
||||
})
|
||||
@@ -319,6 +323,20 @@ export async function fetchAdminConsignmentInventories(query: JsonObject = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 寄售仓库的“通过审核入库时间”按自然日筛选。
|
||||
* 只接受 YYYY-MM-DD,并通过 UTC roundtrip 拦截 2026-02-31 这类会被 Date 溢出的日期。
|
||||
*/
|
||||
export function normalizeInventoryDate(rawValue: unknown) {
|
||||
const normalized = String(rawValue || '').trim()
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) return undefined
|
||||
|
||||
const parsed = new Date(`${normalized}T00:00:00.000Z`)
|
||||
return Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== normalized
|
||||
? undefined
|
||||
: normalized
|
||||
}
|
||||
|
||||
export async function fetchAdminConsignmentStats() {
|
||||
return getConsignmentInventoryStats()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
@@ -28,7 +29,9 @@ import {
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
||||
import { buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useState } from 'react'
|
||||
@@ -148,6 +151,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
const [series, setSeries] = useState<string>('')
|
||||
const [diamondLevel, setDiamondLevel] = useState<number>(0)
|
||||
const [allowMatching, setAllowMatching] = useState<string>('')
|
||||
const [inboundRange, setInboundRange] = useState<[string, string] | null>(null)
|
||||
const [keyword, setKeyword] = useState<string>('')
|
||||
const [page, setPage] = useState<number>(1)
|
||||
const [pageSize, setPageSize] = useState<number>(20)
|
||||
@@ -205,6 +209,8 @@ export default function ConsignmentInventoryPanel() {
|
||||
series,
|
||||
diamondLevel,
|
||||
allowMatching,
|
||||
inboundRange?.[0],
|
||||
inboundRange?.[1],
|
||||
keyword,
|
||||
page,
|
||||
pageSize,
|
||||
@@ -217,6 +223,8 @@ export default function ConsignmentInventoryPanel() {
|
||||
series: series || undefined,
|
||||
diamondLevel: diamondLevel > 0 ? diamondLevel : undefined,
|
||||
allowMatching: allowMatching === 'yes' ? true : allowMatching === 'no' ? false : undefined,
|
||||
dateFrom: inboundRange?.[0],
|
||||
dateTo: inboundRange?.[1],
|
||||
keyword: keyword || undefined,
|
||||
page,
|
||||
pageSize,
|
||||
@@ -835,6 +843,21 @@ export default function ConsignmentInventoryPanel() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<AdminRangePicker
|
||||
allowClear
|
||||
style={{ width: 250 }}
|
||||
placeholder={['入库开始日期', '入库结束日期']}
|
||||
value={inboundRange ? [dayjs(inboundRange[0]), dayjs(inboundRange[1])] : null}
|
||||
onChange={(dates) => {
|
||||
setInboundRange(
|
||||
dates?.[0] && dates[1]
|
||||
? [dates[0].format(ADMIN_DATE_FORMAT), dates[1].format(ADMIN_DATE_FORMAT)]
|
||||
: null,
|
||||
)
|
||||
setPage(1)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input.Search
|
||||
placeholder="搜索物品名/编号/收购人/店铺/卖家"
|
||||
allowClear
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
test('寄售仓库必须提供通过审核入库时间范围筛选,并透传到库存查询', () => {
|
||||
const source = readFileSync(
|
||||
new URL('../src/pages/admin/panels/ConsignmentInventoryPanel.tsx', import.meta.url).pathname,
|
||||
'utf8',
|
||||
)
|
||||
|
||||
assert.ok(
|
||||
source.includes("from '@/components/admin/AdminDatePicker'"),
|
||||
'必须复用后台日期范围控件',
|
||||
)
|
||||
assert.ok(source.includes("placeholder={['入库开始日期', '入库结束日期']}"), '必须明确筛选口径')
|
||||
assert.ok(source.includes('dateFrom: inboundRange?.[0]'), '开始日期必须透传后端')
|
||||
assert.ok(source.includes('dateTo: inboundRange?.[1]'), '结束日期必须透传后端')
|
||||
assert.ok(
|
||||
source.includes('inboundRange?.[0],\n inboundRange?.[1],'),
|
||||
'日期变化必须进入 React Query key 并触发重新查询',
|
||||
)
|
||||
assert.ok(
|
||||
source
|
||||
.slice(source.indexOf('<AdminRangePicker'), source.indexOf('<Input.Search'))
|
||||
.includes('setPage(1)'),
|
||||
'修改日期筛选后必须回到第一页',
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user