增加接单分类和大厅分类配额
This commit is contained in:
@@ -101,5 +101,21 @@ test('大厅容量先确定全局候选集合,再应用页面筛选条件', ()
|
||||
assert.match(query.whereClause, /LIMIT \$4/)
|
||||
const candidateSql = query.whereClause.slice(query.whereClause.indexOf('candidate.status'))
|
||||
assert.doesNotMatch(candidateSql, /candidate\.category_id|candidate.*测试商品/)
|
||||
assert.deepEqual(query.params, [['open'], '%测试商品%', 3, 50, '2026-08-21T00:00:00.000Z'])
|
||||
assert.deepEqual(query.params, [['open'], '%测试商品%', 3, 50, '{}', '2026-08-21T00:00:00.000Z'])
|
||||
})
|
||||
|
||||
test('后台接单工单支持按接单分类筛选', () => {
|
||||
const query = buildWorkOrderWhere({ categoryId: 7 })
|
||||
assert.match(query.whereClause, /wo\.category_id = \$1/)
|
||||
assert.deepEqual(query.params, [7])
|
||||
})
|
||||
|
||||
test('大厅候选按分类上限取队首后再受全局上限限制', () => {
|
||||
const query = buildWorkOrderWhere({
|
||||
hallCandidateLimit: 20,
|
||||
hallCategoryLimits: { '1': 3, '2': 5 },
|
||||
})
|
||||
assert.match(query.whereClause, /ROW_NUMBER\(\) OVER/)
|
||||
assert.match(query.whereClause, /category_rank/)
|
||||
assert.deepEqual(query.params, [20, '{"1":3,"2":5}'])
|
||||
})
|
||||
|
||||
@@ -431,6 +431,7 @@ export type ListInput = {
|
||||
workerSharingId?: number
|
||||
/** 大厅候选集合的总展示上限;筛选与分页在候选集合内执行。 */
|
||||
hallCandidateLimit?: number
|
||||
hallCategoryLimits?: Record<string, number>
|
||||
visibleAfterIso?: string
|
||||
/** 抢单大厅隐藏已被拼完的工单。 */
|
||||
excludeFilledSharing?: boolean
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function grabWorkOrder(input: {
|
||||
depositAmount: number
|
||||
maxActiveOrders: number
|
||||
hallCandidateLimit?: number
|
||||
hallCategoryLimits?: Record<string, number>
|
||||
visibleAfterIso?: string
|
||||
deadlineAt: string | null
|
||||
now: string
|
||||
@@ -42,6 +43,7 @@ export async function grabWorkOrder(input: {
|
||||
!(await isWorkOrderWithinHallCapacityWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
hallCandidateLimit: input.hallCandidateLimit,
|
||||
...(input.hallCategoryLimits ? { hallCategoryLimits: input.hallCategoryLimits } : {}),
|
||||
visibleAfterIso: input.visibleAfterIso,
|
||||
}))
|
||||
) {
|
||||
|
||||
@@ -226,6 +226,7 @@ export async function listWorkOrders({
|
||||
categoryId = 0,
|
||||
workerSharingId = 0,
|
||||
hallCandidateLimit = 0,
|
||||
hallCategoryLimits = {},
|
||||
visibleAfterIso = '',
|
||||
excludeFilledSharing = false,
|
||||
vipEvidencePending = false,
|
||||
@@ -247,6 +248,7 @@ export async function listWorkOrders({
|
||||
categoryId,
|
||||
workerSharingId,
|
||||
hallCandidateLimit,
|
||||
hallCategoryLimits,
|
||||
visibleAfterIso,
|
||||
excludeFilledSharing,
|
||||
vipEvidencePending,
|
||||
@@ -652,6 +654,7 @@ export function buildWorkOrderWhere({
|
||||
categoryId = 0,
|
||||
workerSharingId = 0,
|
||||
hallCandidateLimit = 0,
|
||||
hallCategoryLimits = {},
|
||||
visibleAfterIso = '',
|
||||
excludeFilledSharing = false,
|
||||
vipEvidencePending = false,
|
||||
@@ -843,7 +846,9 @@ export function buildWorkOrderWhere({
|
||||
}
|
||||
if (hallCandidateLimit > 0) {
|
||||
params.push(hallCandidateLimit)
|
||||
filters.push(`wo.id IN (${buildHallCandidateIdsSql(`$${params.length}`)})`)
|
||||
const limitPlaceholder = `$${params.length}`
|
||||
params.push(JSON.stringify(hallCategoryLimits))
|
||||
filters.push(`wo.id IN (${buildHallCandidateIdsSql(limitPlaceholder, `$${params.length}`)})`)
|
||||
}
|
||||
if (visibleAfterIso) {
|
||||
params.push(visibleAfterIso)
|
||||
@@ -863,23 +868,34 @@ export function buildWorkOrderWhere({
|
||||
}
|
||||
|
||||
/** 大厅容量先选出全局队首,分类、关键词和 VIP 延迟只能在该集合内继续筛选。 */
|
||||
function buildHallCandidateIdsSql(limitPlaceholder: string) {
|
||||
function buildHallCandidateIdsSql(limitPlaceholder: string, categoryLimitsPlaceholder: string) {
|
||||
return `
|
||||
SELECT candidate.id
|
||||
FROM work_orders candidate
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
work_order_id,
|
||||
SUM(quantity) FILTER (WHERE status != 'cancelled') AS active_share_quantity
|
||||
FROM work_order_shares
|
||||
GROUP BY work_order_id
|
||||
) candidate_shares ON candidate_shares.work_order_id = candidate.id
|
||||
WHERE candidate.status = 'open'
|
||||
AND (
|
||||
candidate.sharing_enabled IS NOT TRUE
|
||||
OR COALESCE(candidate_shares.active_share_quantity, 0) < candidate.sharing_total_quantity
|
||||
)
|
||||
ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC
|
||||
SELECT ranked.id
|
||||
FROM (
|
||||
SELECT candidate.id, candidate.category_id, candidate.hall_queued_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY candidate.category_id
|
||||
ORDER BY candidate.hall_queued_at ASC NULLS LAST, candidate.id ASC
|
||||
) AS category_rank
|
||||
FROM work_orders candidate
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
work_order_id,
|
||||
SUM(quantity) FILTER (WHERE status != 'cancelled') AS active_share_quantity
|
||||
FROM work_order_shares
|
||||
GROUP BY work_order_id
|
||||
) candidate_shares ON candidate_shares.work_order_id = candidate.id
|
||||
WHERE candidate.status = 'open'
|
||||
AND (
|
||||
candidate.sharing_enabled IS NOT TRUE
|
||||
OR COALESCE(candidate_shares.active_share_quantity, 0) < candidate.sharing_total_quantity
|
||||
)
|
||||
) ranked
|
||||
WHERE ranked.category_rank <= COALESCE(
|
||||
NULLIF(${categoryLimitsPlaceholder}::jsonb ->> ranked.category_id::text, '')::int,
|
||||
${limitPlaceholder}
|
||||
)
|
||||
ORDER BY ranked.hall_queued_at ASC NULLS LAST, ranked.id ASC
|
||||
LIMIT ${limitPlaceholder}
|
||||
`
|
||||
}
|
||||
@@ -887,7 +903,12 @@ function buildHallCandidateIdsSql(limitPlaceholder: string) {
|
||||
/** 抢单操作在事务中复核容量与当前打手的可见延迟,避免通过订单 ID 绕过大厅队列。 */
|
||||
export async function isWorkOrderWithinHallCapacityWithClient(
|
||||
client: PoolClient,
|
||||
input: { workOrderId: number; hallCandidateLimit: number; visibleAfterIso?: string | undefined },
|
||||
input: {
|
||||
workOrderId: number
|
||||
hallCandidateLimit: number
|
||||
hallCategoryLimits?: Record<string, number>
|
||||
visibleAfterIso?: string | undefined
|
||||
},
|
||||
) {
|
||||
const result = await client.query<{ visible: boolean }>(
|
||||
`
|
||||
@@ -895,13 +916,14 @@ export async function isWorkOrderWithinHallCapacityWithClient(
|
||||
SELECT 1
|
||||
FROM work_orders wo
|
||||
WHERE wo.id = $1
|
||||
AND wo.id IN (${buildHallCandidateIdsSql('$2')})
|
||||
AND ($3::timestamptz IS NULL OR wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $3)
|
||||
AND wo.id IN (${buildHallCandidateIdsSql('$2', '$3')})
|
||||
AND ($4::timestamptz IS NULL OR wo.hall_queued_at IS NULL OR wo.hall_queued_at <= $4)
|
||||
) AS visible
|
||||
`,
|
||||
[
|
||||
input.workOrderId,
|
||||
input.hallCandidateLimit,
|
||||
JSON.stringify(input.hallCategoryLimits || {}),
|
||||
input.visibleAfterIso ? input.visibleAfterIso : null,
|
||||
],
|
||||
)
|
||||
|
||||
@@ -103,6 +103,7 @@ export async function joinWorkOrderShare(input: {
|
||||
takeRemaining?: boolean
|
||||
maxActiveOrders: number
|
||||
hallCandidateLimit?: number
|
||||
hallCategoryLimits?: Record<string, number>
|
||||
visibleAfterIso?: string
|
||||
now: string
|
||||
}): Promise<{
|
||||
@@ -137,6 +138,7 @@ export async function joinWorkOrderShare(input: {
|
||||
!(await isWorkOrderWithinHallCapacityWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
hallCandidateLimit: input.hallCandidateLimit,
|
||||
...(input.hallCategoryLimits ? { hallCategoryLimits: input.hallCategoryLimits } : {}),
|
||||
visibleAfterIso: input.visibleAfterIso,
|
||||
}))
|
||||
) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import {
|
||||
createWorkOrder,
|
||||
createWorkOrderEvent,
|
||||
getWorkCategoryById,
|
||||
listWorkOrdersByPlatformOrderId,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
@@ -47,6 +48,18 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) {
|
||||
: 0
|
||||
: normalizeAmountFen(payload.requiredDepositAmount, 0)
|
||||
const paymentFen = input.paymentAmount
|
||||
const requestedCategoryId = Number(payload.categoryId || payload.category_id || 0)
|
||||
let categoryId = defaults.category?.id || null
|
||||
if (requestedCategoryId > 0) {
|
||||
const category = await getWorkCategoryById(requestedCategoryId)
|
||||
if (!category || category.status !== 'active') {
|
||||
throw createHttpError('所选接单分类不存在或已停用', {
|
||||
statusCode: 409,
|
||||
errorCode: 'mock_work_order_category_invalid',
|
||||
})
|
||||
}
|
||||
categoryId = Number(category.id)
|
||||
}
|
||||
const materialComplete = payload.materialComplete === true || payload.materialComplete === 'true'
|
||||
const fields = normalizeRequirementFields(payload.fields)
|
||||
const { platformOrderId, productName, shop } = input
|
||||
@@ -92,7 +105,7 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) {
|
||||
workOrderNo: randomId('WO'),
|
||||
platformOrderId,
|
||||
productName,
|
||||
categoryId: defaults.category?.id || null,
|
||||
categoryId,
|
||||
status: materialComplete ? WORK_ORDER_STATUS.UNASSIGNED : WORK_ORDER_STATUS.PENDING_MATERIAL,
|
||||
rewardAmount,
|
||||
requiredDepositAmount,
|
||||
|
||||
@@ -250,6 +250,7 @@ export async function publishAdminWorkOrder(workOrderId: number | string, actorN
|
||||
workOrderId: Number(workOrder.id),
|
||||
statuses: [WORK_ORDER_STATUS.OPEN],
|
||||
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
|
||||
excludeFilledSharing: true,
|
||||
})
|
||||
return {
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const workOrderId = normalizeOptionalId(query.workOrderId ?? query.work_order_id) || 0
|
||||
const categoryId = normalizeOptionalId(query.categoryId ?? query.category_id) || 0
|
||||
const workerId = normalizeOptionalId(query.workerId ?? query.worker_id) || 0
|
||||
const workerLevelIds = normalizeStatuses(query.workerLevelIds ?? query.worker_level_ids)
|
||||
.map(normalizeOptionalId)
|
||||
@@ -43,6 +44,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
const filters = {
|
||||
statuses,
|
||||
workOrderId,
|
||||
categoryId,
|
||||
keyword: String(query.keyword || '').trim(),
|
||||
workerSharingId: workerId,
|
||||
workerLevelIds,
|
||||
|
||||
+44
-25
@@ -5,9 +5,44 @@ import { getWorkerHallConfig, saveWorkerHallConfig } from './worker-hall-config-
|
||||
|
||||
export async function getAdminWorkerPlatformSummary() {
|
||||
const hallConfig = getWorkerHallConfig()
|
||||
const [pendingWorkers, pendingMaterial, openOrders, inProgressOrders] = await Promise.all([
|
||||
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }),
|
||||
listWorkOrders({ page: 1, pageSize: 1, statuses: [WORK_ORDER_STATUS.PENDING_MATERIAL] }),
|
||||
const [pendingWorkers, pendingMaterial, openOrders, visibleOpenOrders, inProgressOrders] =
|
||||
await Promise.all([
|
||||
listWorkerUsers({ page: 1, pageSize: 1, status: 'pending_review' }),
|
||||
listWorkOrders({ page: 1, pageSize: 1, statuses: [WORK_ORDER_STATUS.PENDING_MATERIAL] }),
|
||||
listWorkOrders({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
statuses: [WORK_ORDER_STATUS.OPEN],
|
||||
excludeFilledSharing: true,
|
||||
}),
|
||||
listWorkOrders({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
statuses: [WORK_ORDER_STATUS.OPEN],
|
||||
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
|
||||
excludeFilledSharing: true,
|
||||
}),
|
||||
listWorkOrders({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
statuses: [WORK_ORDER_STATUS.IN_PROGRESS],
|
||||
useOperationalStatus: true,
|
||||
}),
|
||||
])
|
||||
return {
|
||||
pendingWorkerCount: pendingWorkers.total,
|
||||
pendingMaterialCount: pendingMaterial.total,
|
||||
openOrderCount: visibleOpenOrders.total,
|
||||
queuedHallOrderCount: Math.max(0, openOrders.total - visibleOpenOrders.total),
|
||||
hallMaxVisibleOrders: hallConfig.maxVisibleOrders,
|
||||
inProgressOrderCount: inProgressOrders.total,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminWorkerHallConfig() {
|
||||
const config = getWorkerHallConfig()
|
||||
const [openOrders, visibleOpenOrders] = await Promise.all([
|
||||
listWorkOrders({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
@@ -17,32 +52,16 @@ export async function getAdminWorkerPlatformSummary() {
|
||||
listWorkOrders({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
statuses: [WORK_ORDER_STATUS.IN_PROGRESS],
|
||||
useOperationalStatus: true,
|
||||
statuses: [WORK_ORDER_STATUS.OPEN],
|
||||
hallCandidateLimit: config.maxVisibleOrders,
|
||||
hallCategoryLimits: config.categoryMaxVisibleOrders,
|
||||
excludeFilledSharing: true,
|
||||
}),
|
||||
])
|
||||
return {
|
||||
pendingWorkerCount: pendingWorkers.total,
|
||||
pendingMaterialCount: pendingMaterial.total,
|
||||
openOrderCount: Math.min(openOrders.total, hallConfig.maxVisibleOrders),
|
||||
queuedHallOrderCount: Math.max(0, openOrders.total - hallConfig.maxVisibleOrders),
|
||||
hallMaxVisibleOrders: hallConfig.maxVisibleOrders,
|
||||
inProgressOrderCount: inProgressOrders.total,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAdminWorkerHallConfig() {
|
||||
const config = getWorkerHallConfig()
|
||||
const openOrders = await listWorkOrders({
|
||||
page: 1,
|
||||
pageSize: 1,
|
||||
statuses: [WORK_ORDER_STATUS.OPEN],
|
||||
excludeFilledSharing: true,
|
||||
})
|
||||
return {
|
||||
...config,
|
||||
visibleOrderCount: Math.min(openOrders.total, config.maxVisibleOrders),
|
||||
queuedOrderCount: Math.max(0, openOrders.total - config.maxVisibleOrders),
|
||||
visibleOrderCount: visibleOpenOrders.total,
|
||||
queuedOrderCount: Math.max(0, openOrders.total - visibleOpenOrders.total),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
|
||||
takeRemaining: true,
|
||||
maxActiveOrders: permissions.maxActiveOrders,
|
||||
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
|
||||
visibleAfterIso,
|
||||
now,
|
||||
})
|
||||
@@ -91,6 +92,7 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session:
|
||||
depositAmount: freezeAmount,
|
||||
maxActiveOrders: permissions.maxActiveOrders,
|
||||
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
|
||||
visibleAfterIso,
|
||||
deadlineAt: resolveWorkOrderDeadlineAt(workOrder, now),
|
||||
now,
|
||||
@@ -143,6 +145,7 @@ export async function joinWorkerSharingOrder(
|
||||
quantity,
|
||||
maxActiveOrders: permissions.maxActiveOrders,
|
||||
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
|
||||
visibleAfterIso,
|
||||
now,
|
||||
})
|
||||
|
||||
@@ -7,17 +7,36 @@ import {
|
||||
} from './worker-hall-config-service.js'
|
||||
|
||||
test('大厅配置默认展示 50 个订单', () => {
|
||||
assert.deepEqual(createDefaultWorkerHallConfig(), { maxVisibleOrders: 50 })
|
||||
assert.deepEqual(normalizeWorkerHallConfig({}), { maxVisibleOrders: 50 })
|
||||
assert.deepEqual(createDefaultWorkerHallConfig(), {
|
||||
maxVisibleOrders: 50,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({}), {
|
||||
maxVisibleOrders: 50,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
})
|
||||
|
||||
test('大厅配置只接受 1 到 500 的整数', () => {
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 88 }), { maxVisibleOrders: 88 })
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 88 }), {
|
||||
maxVisibleOrders: 88,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ max_visible_orders: 999 }), {
|
||||
maxVisibleOrders: 500,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 0 }), {
|
||||
maxVisibleOrders: 1,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 0 }), { maxVisibleOrders: 1 })
|
||||
assert.deepEqual(normalizeWorkerHallConfig({ maxVisibleOrders: 1.5 }), {
|
||||
maxVisibleOrders: 50,
|
||||
categoryMaxVisibleOrders: {},
|
||||
})
|
||||
assert.deepEqual(
|
||||
normalizeWorkerHallConfig({ categoryMaxVisibleOrders: { '1': 12, '2': 0, '3': 501 } })
|
||||
.categoryMaxVisibleOrders,
|
||||
{ '1': 12 },
|
||||
)
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-sto
|
||||
|
||||
export type WorkerHallConfig = {
|
||||
maxVisibleOrders: number
|
||||
categoryMaxVisibleOrders: Record<string, number>
|
||||
}
|
||||
|
||||
export function getWorkerHallConfig(): WorkerHallConfig {
|
||||
@@ -23,7 +24,7 @@ export async function saveWorkerHallConfig(rawValue: unknown): Promise<WorkerHal
|
||||
}
|
||||
|
||||
export function createDefaultWorkerHallConfig(): WorkerHallConfig {
|
||||
return { maxVisibleOrders: 50 }
|
||||
return { maxVisibleOrders: 50, categoryMaxVisibleOrders: {} }
|
||||
}
|
||||
|
||||
export function normalizeWorkerHallConfig(rawValue: unknown): WorkerHallConfig {
|
||||
@@ -31,9 +32,24 @@ export function normalizeWorkerHallConfig(rawValue: unknown): WorkerHallConfig {
|
||||
const value = Number(source.maxVisibleOrders ?? source.max_visible_orders)
|
||||
return {
|
||||
maxVisibleOrders: Number.isInteger(value) ? Math.min(500, Math.max(1, value)) : 50,
|
||||
categoryMaxVisibleOrders: normalizeCategoryMaxVisibleOrders(
|
||||
source.categoryMaxVisibleOrders ?? source.category_max_visible_orders,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCategoryMaxVisibleOrders(value: unknown): Record<string, number> {
|
||||
if (!isPlainObject(value)) return {}
|
||||
return Object.entries(value).reduce<Record<string, number>>((result, [categoryId, raw]) => {
|
||||
const id = Number(categoryId)
|
||||
const limit = Number(raw)
|
||||
if (Number.isInteger(id) && id > 0 && Number.isInteger(limit) && limit >= 1 && limit <= 500) {
|
||||
result[String(id)] = limit
|
||||
}
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work
|
||||
keyword: String(query.keyword || '').trim(),
|
||||
categoryId: categoryId || 0,
|
||||
hallCandidateLimit: hallConfig.maxVisibleOrders,
|
||||
hallCategoryLimits: hallConfig.categoryMaxVisibleOrders,
|
||||
visibleAfterIso,
|
||||
excludeFilledSharing: true,
|
||||
pinnedFirst: true,
|
||||
|
||||
Reference in New Issue
Block a user