feat(oa-board): refine flow detail filters and layout
This commit is contained in:
parent
9b5f2d9ee8
commit
1cff836933
@ -28,6 +28,98 @@ function applyHeaderStyle(worksheet: ExcelJS.Worksheet) {
|
||||
}
|
||||
}
|
||||
|
||||
type TimelineMetricMap = Record<
|
||||
string,
|
||||
{
|
||||
targetDays: number | null
|
||||
actualDays: number | null
|
||||
}
|
||||
>
|
||||
|
||||
type TimingDetailDefinition = {
|
||||
header: string
|
||||
metricKey: string
|
||||
valueType: 'target' | 'actual'
|
||||
}
|
||||
|
||||
const TIMING_DETAIL_DEFINITIONS: TimingDetailDefinition[] = [
|
||||
{ header: '目标-外观', metricKey: 'exterior', valueType: 'target' },
|
||||
{ header: '实际-外观', metricKey: 'exterior', valueType: 'actual' },
|
||||
{ header: '目标-结构', metricKey: 'structure', valueType: 'target' },
|
||||
{ header: '实际-结构', metricKey: 'structure', valueType: 'actual' },
|
||||
{ header: '目标-打样', metricKey: 'sample_prepare', valueType: 'target' },
|
||||
{ header: '实际-打样', metricKey: 'sample_prepare', valueType: 'actual' },
|
||||
{ header: '目标-审样', metricKey: 'sample_review', valueType: 'target' },
|
||||
{ header: '实际-审样', metricKey: 'sample_review', valueType: 'actual' },
|
||||
{ header: '目标-生产', metricKey: 'production', valueType: 'target' },
|
||||
{ header: '实际-生产', metricKey: 'production', valueType: 'actual' },
|
||||
{ header: '目标-商品资料', metricKey: 'material', valueType: 'target' },
|
||||
{ header: '实际-商品资料', metricKey: 'material', valueType: 'actual' },
|
||||
{ header: '企划更新市场分析', metricKey: 'market_analysis', valueType: 'actual' },
|
||||
{ header: '目标-首单下单(线下签订合同)', metricKey: 'first_order', valueType: 'target' },
|
||||
{ header: '实际-首单下单(线下签订合同)', metricKey: 'first_order', valueType: 'actual' },
|
||||
{ header: '目标-产前样确认', metricKey: 'pre_production', valueType: 'target' },
|
||||
{ header: '实际-产前样确认', metricKey: 'pre_production', valueType: 'actual' },
|
||||
{ header: '目标-出货', metricKey: 'shipment', valueType: 'target' },
|
||||
{ header: '实际-出货', metricKey: 'shipment', valueType: 'actual' },
|
||||
{ header: '目标-核价', metricKey: 'pricing', valueType: 'target' },
|
||||
{ header: '实际-核价', metricKey: 'pricing', valueType: 'actual' },
|
||||
{ header: '实际-核价后并行用时', metricKey: 'pricing_parallel', valueType: 'actual' },
|
||||
{ header: '目标-送仓质检', metricKey: 'warehouse_qc', valueType: 'target' },
|
||||
{ header: '实际时效-送仓质检', metricKey: 'warehouse_qc', valueType: 'actual' },
|
||||
{ header: '目标-安装图纸', metricKey: 'install_manual', valueType: 'target' },
|
||||
{ header: '实际-安装图纸', metricKey: 'install_manual', valueType: 'actual' },
|
||||
{ header: '目标-寄样', metricKey: 'sample_send', valueType: 'target' },
|
||||
{ header: '实际-寄样', metricKey: 'sample_send', valueType: 'actual' },
|
||||
{ header: '目标-出图', metricKey: 'art', valueType: 'target' },
|
||||
{ header: '实际-出图', metricKey: 'art', valueType: 'actual' },
|
||||
{ header: '目标-限价', metricKey: 'pricing_flow', valueType: 'target' },
|
||||
{ header: '实际-限价', metricKey: 'pricing_flow', valueType: 'actual' },
|
||||
{ header: '目标-运营上架', metricKey: 'listing', valueType: 'target' },
|
||||
{ header: '实际-运营上架', metricKey: 'listing', valueType: 'actual' },
|
||||
]
|
||||
|
||||
function formatDayValue(value: number | null | undefined) {
|
||||
if (value === null || value === undefined || Number.isNaN(value)) return ''
|
||||
return value
|
||||
}
|
||||
|
||||
function createTimelineMetricMap(
|
||||
metrics: Array<{ key: string; targetDays: number | null; actualDays: number | null }>,
|
||||
): TimelineMetricMap {
|
||||
return Object.fromEntries(
|
||||
metrics.map((metric) => [
|
||||
metric.key,
|
||||
{ targetDays: metric.targetDays, actualDays: metric.actualDays },
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function readMultiValueParam(searchParams: URLSearchParams, key: string) {
|
||||
return searchParams
|
||||
.getAll(key)
|
||||
.flatMap((value) => value.split(','))
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function formatDateOnly(value: string | null | undefined) {
|
||||
const text = String(value ?? '').trim()
|
||||
return text ? text.slice(0, 10) : ''
|
||||
}
|
||||
|
||||
function readMetricValue(
|
||||
metrics: TimelineMetricMap,
|
||||
key: string,
|
||||
valueType: 'target' | 'actual',
|
||||
) {
|
||||
const metric = metrics[key]
|
||||
if (!metric) return ''
|
||||
return valueType === 'target'
|
||||
? formatDayValue(metric.targetDays)
|
||||
: formatDayValue(metric.actualDays)
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return handleRequest(
|
||||
request,
|
||||
@ -42,9 +134,16 @@ export async function GET(request: NextRequest) {
|
||||
const dashboard = await oaBoardService.getDashboard({
|
||||
keyword: searchParams.get('keyword') || undefined,
|
||||
approvalNumber: searchParams.get('approvalNumber') || undefined,
|
||||
overallStatus: searchParams.get('overallStatus') || undefined,
|
||||
overallStatus: readMultiValueParam(searchParams, 'overallStatus'),
|
||||
createdDateStart: searchParams.get('createdDateStart') || undefined,
|
||||
createdDateEnd: searchParams.get('createdDateEnd') || undefined,
|
||||
productType: searchParams.get('productType') || undefined,
|
||||
approvalStage: searchParams.get('approvalStage') || undefined,
|
||||
productPlanner: searchParams.get('productPlanner') || undefined,
|
||||
structureDesigner: searchParams.get('structureDesigner') || undefined,
|
||||
appearanceDesigner: searchParams.get('appearanceDesigner') || undefined,
|
||||
purchaseOwner: searchParams.get('purchaseOwner') || undefined,
|
||||
spuOwner: searchParams.get('spuOwner') || undefined,
|
||||
owner: searchParams.get('owner') || undefined,
|
||||
nodeName: searchParams.get('nodeName') || undefined,
|
||||
timeoutOnly: searchParams.get('timeoutOnly') === '1',
|
||||
@ -59,6 +158,7 @@ export async function GET(request: NextRequest) {
|
||||
{ header: '序号', key: 'index', width: 8 },
|
||||
{ header: '审批编号', key: 'approvalNumber', width: 24 },
|
||||
{ header: '开款编号', key: 'openingCode', width: 22 },
|
||||
{ header: '创建日期', key: 'createdDate', width: 14 },
|
||||
{ header: '总流程状态', key: 'totalFlowStatusLabel', width: 12 },
|
||||
{ header: '流程审批阶段', key: 'approvalStageDisplay', width: 20 },
|
||||
{ header: '当前负责人', key: 'currentOwnerDisplay', width: 18 },
|
||||
@ -85,6 +185,7 @@ export async function GET(request: NextRequest) {
|
||||
index: index + 1,
|
||||
approvalNumber: item.boardKey,
|
||||
openingCode: item.formSummary.openingCode || item.openingCode || '',
|
||||
createdDate: formatDateOnly(item.createdDate),
|
||||
totalFlowStatusLabel: item.formSummary.totalFlowStatusLabel || '',
|
||||
approvalStageDisplay: item.formSummary.approvalStageDisplay || item.currentApprovalStageName || '',
|
||||
currentOwnerDisplay: item.formSummary.currentOwnerDisplay || '',
|
||||
@ -109,9 +210,9 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const detail = workbook.addWorksheet('流程明细')
|
||||
detail.columns = [
|
||||
{ header: '审批编号', key: 'approvalNumber', width: 24 },
|
||||
{ header: '款式图片', key: 'firstImageUrl', width: 54 },
|
||||
{ header: '开款编号', key: 'openingCode', width: 22 },
|
||||
{ header: '创建日期', key: 'createdDate', width: 14 },
|
||||
{ header: '总流程状态', key: 'totalFlowStatusLabel', width: 14 },
|
||||
{ header: '流程审批阶段', key: 'approvalStageDisplay', width: 20 },
|
||||
{ header: '当前审批节点', key: 'currentNodeDisplay', width: 24 },
|
||||
@ -147,19 +248,22 @@ export async function GET(request: NextRequest) {
|
||||
{ header: 'SPU', key: 'spu', width: 14 },
|
||||
{ header: '开款品牌', key: 'openingBrand', width: 16 },
|
||||
{ header: '创建人', key: 'creatorLabel', width: 14 },
|
||||
{ header: '创建日期', key: 'createdDate', width: 24 },
|
||||
{ header: '结束日期', key: 'finishedDate', width: 24 },
|
||||
{ header: '总目标时效', key: 'overallTargetHours', width: 14 },
|
||||
{ header: '总实际时效', key: 'overallActualHours', width: 14 },
|
||||
{ header: '是否超时', key: 'isOvertime', width: 10 },
|
||||
{ header: '详细时效JSON', key: 'timelineMetrics', width: 80 },
|
||||
...TIMING_DETAIL_DEFINITIONS.map((field) => ({
|
||||
header: field.header,
|
||||
key: field.header,
|
||||
width: 18,
|
||||
})),
|
||||
]
|
||||
|
||||
dashboard.items.forEach((item) => {
|
||||
const metricMap = createTimelineMetricMap(item.timelineMetrics)
|
||||
detail.addRow({
|
||||
approvalNumber: item.boardKey,
|
||||
firstImageUrl: item.styleImages[0]?.url || '',
|
||||
openingCode: item.formSummary.openingCode || item.openingCode || '',
|
||||
createdDate: formatDateOnly(item.createdDate),
|
||||
totalFlowStatusLabel: item.formSummary.totalFlowStatusLabel || '',
|
||||
approvalStageDisplay: item.formSummary.approvalStageDisplay || item.currentApprovalStageName || '',
|
||||
currentNodeDisplay: item.formSummary.currentNodeDisplay || '',
|
||||
@ -195,12 +299,15 @@ export async function GET(request: NextRequest) {
|
||||
spu: item.formSummary.spu || item.spu || '',
|
||||
openingBrand: item.formSummary.openingBrand || '',
|
||||
creatorLabel: item.originatorDeptName || item.boardKeyType || '',
|
||||
createdDate: item.createdDate || '',
|
||||
finishedDate: item.finishedDate || '',
|
||||
overallTargetHours: item.overallTargetHours ?? '',
|
||||
overallActualHours: item.overallActualHours ?? '',
|
||||
isOvertime: item.overallTimeoutFlag ? '是' : '否',
|
||||
timelineMetrics: JSON.stringify(item.timelineMetrics, null, 0),
|
||||
...Object.fromEntries(
|
||||
TIMING_DETAIL_DEFINITIONS.map((field) => [
|
||||
field.header,
|
||||
readMetricValue(metricMap, field.metricKey, field.valueType),
|
||||
]),
|
||||
),
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,10 @@
|
||||
import { requireSession } from '@/lib/server-session'
|
||||
import { createEmptyOaBoardDashboard, oaBoardService } from '@/server/services/oa-board.service'
|
||||
import {
|
||||
createEmptyOaBoardDashboard,
|
||||
createMockOaBoardDashboard,
|
||||
oaBoardService,
|
||||
} from '@/server/services/oa-board.service'
|
||||
import { OA_BOARD_DEFAULT_TRACKED_STATUSES } from '@/server/services/oa-board.constants'
|
||||
import OaBoardClient from './OaBoardClient'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
@ -12,38 +17,91 @@ export default async function OaBoardPage({
|
||||
await requireSession('/login')
|
||||
const params = searchParams ? await searchParams : {}
|
||||
|
||||
const keyword = typeof params.keyword === 'string' ? params.keyword : ''
|
||||
const approvalNumber = typeof params.approvalNumber === 'string' ? params.approvalNumber : ''
|
||||
const overallStatus = typeof params.overallStatus === 'string' ? params.overallStatus : ''
|
||||
const productType = typeof params.productType === 'string' ? params.productType : ''
|
||||
const approvalStage = typeof params.approvalStage === 'string' ? params.approvalStage : ''
|
||||
const owner = typeof params.owner === 'string' ? params.owner : ''
|
||||
const nodeName = typeof params.nodeName === 'string' ? params.nodeName : ''
|
||||
const readSingleParam = (value: string | string[] | undefined) =>
|
||||
typeof value === 'string' ? value : ''
|
||||
const readMultiParam = (value: string | string[] | undefined) =>
|
||||
(Array.isArray(value) ? value : [value])
|
||||
.flatMap((entry) => String(entry ?? '').split(','))
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const keyword = readSingleParam(params.keyword)
|
||||
const approvalNumber = readSingleParam(params.approvalNumber)
|
||||
const overallStatus = readMultiParam(params.overallStatus)
|
||||
const createdDateStart = readSingleParam(params.createdDateStart)
|
||||
const createdDateEnd = readSingleParam(params.createdDateEnd)
|
||||
const productType = readSingleParam(params.productType)
|
||||
const approvalStage = readSingleParam(params.approvalStage)
|
||||
const productPlanner = readSingleParam(params.productPlanner)
|
||||
const structureDesigner = readSingleParam(params.structureDesigner)
|
||||
const appearanceDesigner = readSingleParam(params.appearanceDesigner)
|
||||
const purchaseOwner = readSingleParam(params.purchaseOwner)
|
||||
const spuOwner = readSingleParam(params.spuOwner)
|
||||
const owner = readSingleParam(params.owner)
|
||||
const nodeName = readSingleParam(params.nodeName)
|
||||
const timeoutOnly = params.timeoutOnly === '1'
|
||||
|
||||
const result = await oaBoardService
|
||||
.getDashboard({
|
||||
keyword: keyword || undefined,
|
||||
approvalNumber: approvalNumber || undefined,
|
||||
overallStatus: overallStatus || undefined,
|
||||
overallStatus:
|
||||
overallStatus.length > 0
|
||||
? overallStatus
|
||||
: [...OA_BOARD_DEFAULT_TRACKED_STATUSES],
|
||||
createdDateStart: createdDateStart || undefined,
|
||||
createdDateEnd: createdDateEnd || undefined,
|
||||
productType: productType || undefined,
|
||||
approvalStage: approvalStage || undefined,
|
||||
productPlanner: productPlanner || undefined,
|
||||
structureDesigner: structureDesigner || undefined,
|
||||
appearanceDesigner: appearanceDesigner || undefined,
|
||||
purchaseOwner: purchaseOwner || undefined,
|
||||
spuOwner: spuOwner || undefined,
|
||||
owner: owner || undefined,
|
||||
nodeName: nodeName || undefined,
|
||||
timeoutOnly,
|
||||
})
|
||||
.then((dashboard) => ({ dashboard, loadErrorMessage: null as string | null }))
|
||||
.catch((error) => {
|
||||
const loadErrorMessage =
|
||||
const fallbackMessage =
|
||||
error instanceof Error
|
||||
? `页面骨架已生成,但读取企划库流程数据失败:${error.message}`
|
||||
: '页面骨架已生成,但读取企划库流程数据失败。'
|
||||
|
||||
const useMockDashboard = process.env.NODE_ENV !== 'production'
|
||||
|
||||
return {
|
||||
dashboard: createEmptyOaBoardDashboard(),
|
||||
loadErrorMessage,
|
||||
dashboard: useMockDashboard ? createMockOaBoardDashboard() : createEmptyOaBoardDashboard(),
|
||||
loadErrorMessage: useMockDashboard
|
||||
? `${fallbackMessage} 当前已切换为本地人工验证数据,方便验证流程明细进度。`
|
||||
: fallbackMessage,
|
||||
}
|
||||
})
|
||||
|
||||
return <OaBoardClient dashboard={result.dashboard} loadErrorMessage={result.loadErrorMessage} />
|
||||
const pageStateKey = JSON.stringify({
|
||||
keyword,
|
||||
approvalNumber,
|
||||
overallStatus,
|
||||
createdDateStart,
|
||||
createdDateEnd,
|
||||
productType,
|
||||
approvalStage,
|
||||
productPlanner,
|
||||
structureDesigner,
|
||||
appearanceDesigner,
|
||||
purchaseOwner,
|
||||
spuOwner,
|
||||
owner,
|
||||
nodeName,
|
||||
timeoutOnly,
|
||||
})
|
||||
|
||||
return (
|
||||
<OaBoardClient
|
||||
key={pageStateKey}
|
||||
dashboard={result.dashboard}
|
||||
loadErrorMessage={result.loadErrorMessage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -83,6 +83,7 @@ function AppShellContent({ children, user }: AppShellProps) {
|
||||
pathname === '/assisted-assembly' || pathname.startsWith('/assisted-assembly/')
|
||||
const isDraftGroupsRoute = pathname === '/draft-groups' || pathname.startsWith('/draft-groups/')
|
||||
const isIntakeRoute = pathname === '/intake' || pathname.startsWith('/intake/')
|
||||
const isOaBoardRoute = pathname === '/intake/oa-board' || pathname.startsWith('/intake/oa-board/')
|
||||
const isWarehouseRoute = pathname === '/warehouse' || pathname.startsWith('/warehouse/')
|
||||
const isWarehouseQualityRoute =
|
||||
pathname === '/warehouse-quality' || pathname.startsWith('/warehouse-quality/')
|
||||
@ -106,7 +107,7 @@ function AppShellContent({ children, user }: AppShellProps) {
|
||||
isWarehouseQualityRoute ||
|
||||
isAiRenderWorkbenchRoute
|
||||
const shouldLockMainViewport =
|
||||
isWorkbenchShellRoute
|
||||
isWorkbenchShellRoute && !isOaBoardRoute
|
||||
|
||||
useEffect(() => {
|
||||
if (user !== undefined) {
|
||||
@ -242,8 +243,12 @@ function AppShellContent({ children, user }: AppShellProps) {
|
||||
<main
|
||||
className={
|
||||
isMobileOrTablet
|
||||
? 'mt-14 h-[calc(100dvh-56px-56px)] min-h-[calc(100dvh-56px-56px)] overflow-hidden overscroll-none'
|
||||
: 'mt-16 h-[calc(100dvh-64px)] min-h-[calc(100dvh-64px)] overflow-hidden overscroll-none'
|
||||
? isOaBoardRoute
|
||||
? 'mt-14 h-[calc(100dvh-56px-56px)] min-h-[calc(100dvh-56px-56px)] overflow-auto overscroll-contain'
|
||||
: 'mt-14 h-[calc(100dvh-56px-56px)] min-h-[calc(100dvh-56px-56px)] overflow-hidden overscroll-none'
|
||||
: isOaBoardRoute
|
||||
? 'mt-16 h-[calc(100dvh-64px)] min-h-[calc(100dvh-64px)] overflow-auto overscroll-contain'
|
||||
: 'mt-16 h-[calc(100dvh-64px)] min-h-[calc(100dvh-64px)] overflow-hidden overscroll-none'
|
||||
}
|
||||
style={{ backgroundColor: isMobileOrTablet ? '#F8F9FA' : 'var(--obs-shell-bg)' }}
|
||||
>
|
||||
|
||||
@ -28,32 +28,57 @@ export function PlatformMetricStrip({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<article
|
||||
key={`${item.label}-${String(item.value)}`}
|
||||
className="flex min-h-[104px] flex-col justify-between rounded-apple-lg border border-black/5 bg-white/60 px-4 py-3"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="text-xs font-apple-medium tracking-apple-wide text-apple-label-tertiary">
|
||||
{item.label}
|
||||
</p>
|
||||
{item.icon ? <span className="text-apple-label-tertiary">{item.icon}</span> : null}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p
|
||||
className={cn(
|
||||
'text-[30px] font-apple-semibold tracking-apple-title2 leading-none',
|
||||
TONE_CLASS_MAP[item.tone ?? 'default'],
|
||||
)}
|
||||
>
|
||||
{item.value}
|
||||
</p>
|
||||
{item.hint ? (
|
||||
<p className="text-xs leading-relaxed text-apple-label-secondary">{item.hint}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
{items.map((item) => {
|
||||
const cardClassName = cn(
|
||||
'flex min-h-[104px] flex-col justify-between rounded-apple-lg border px-4 py-3 text-left transition',
|
||||
item.onClick
|
||||
? 'cursor-pointer border-black/6 bg-white/70 hover:border-black/12 hover:bg-white hover:shadow-apple-sm'
|
||||
: 'border-black/5 bg-white/60',
|
||||
item.isActive ? 'border-[color:var(--color-blue)] bg-[#f7fbff] shadow-apple-sm' : null,
|
||||
)
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<p className="text-xs font-apple-medium tracking-apple-wide text-apple-label-tertiary">
|
||||
{item.label}
|
||||
</p>
|
||||
{item.icon ? <span className="text-apple-label-tertiary">{item.icon}</span> : null}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p
|
||||
className={cn(
|
||||
'text-[30px] font-apple-semibold tracking-apple-title2 leading-none',
|
||||
TONE_CLASS_MAP[item.tone ?? 'default'],
|
||||
)}
|
||||
>
|
||||
{item.value}
|
||||
</p>
|
||||
{item.hint ? (
|
||||
<p className="text-xs leading-relaxed text-apple-label-secondary">{item.hint}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
return item.onClick ? (
|
||||
<button
|
||||
key={`${item.label}-${String(item.value)}`}
|
||||
type="button"
|
||||
onClick={item.onClick}
|
||||
className={cardClassName}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<article
|
||||
key={`${item.label}-${String(item.value)}`}
|
||||
className={cardClassName}
|
||||
>
|
||||
{content}
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@ -21,4 +21,6 @@ export interface PlatformMetricItem {
|
||||
hint?: string | null
|
||||
tone?: 'default' | 'success' | 'warning' | 'danger' | 'info'
|
||||
icon?: ReactNode
|
||||
isActive?: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
@ -48,6 +48,7 @@ type SourceSchemeRow = RowDataPacket & {
|
||||
}
|
||||
|
||||
type SourceTaskRow = RowDataPacket & {
|
||||
id: number
|
||||
processInstanceId: string
|
||||
processCode: string
|
||||
result: string | null
|
||||
@ -445,6 +446,9 @@ function toFlowStatusLabel(status: string | null | undefined, result: string | n
|
||||
if (normalizedStatus === 'RUNNING' || normalizedStatus === 'PROCESSING' || normalizedStatus === 'PENDING') {
|
||||
return '进行中'
|
||||
}
|
||||
if (normalizedStatus === 'REFUSED') {
|
||||
return '已驳回'
|
||||
}
|
||||
if (normalizedStatus === 'TERMINATED') {
|
||||
return '已终止'
|
||||
}
|
||||
@ -462,9 +466,50 @@ function pickActiveTasks(records: NormalizedFlowRecord[]) {
|
||||
const orderRecord = records.find((record) => record.flowType === 'order') ?? null
|
||||
const artRecord = records.find((record) => record.flowType === 'art') ?? null
|
||||
const pricingRecord = records.find((record) => record.flowType === 'pricing') ?? null
|
||||
const mainRecord = records.find((record) => record.flowType === 'main') ?? null
|
||||
|
||||
const runningTasks = (record: NormalizedFlowRecord | null) =>
|
||||
record?.taskRecords.filter((taskRecord) => taskRecord.status === 'RUNNING') ?? []
|
||||
const getTaskSequence = (taskRecord: NormalizedTaskRecord) => {
|
||||
if (
|
||||
taskRecord.metadata &&
|
||||
typeof taskRecord.metadata === 'object' &&
|
||||
!Array.isArray(taskRecord.metadata) &&
|
||||
typeof (taskRecord.metadata as { sequence?: unknown }).sequence === 'number'
|
||||
) {
|
||||
return (taskRecord.metadata as { sequence: number }).sequence
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const getTaskRowId = (taskRecord: NormalizedTaskRecord) => {
|
||||
if (
|
||||
taskRecord.metadata &&
|
||||
typeof taskRecord.metadata === 'object' &&
|
||||
!Array.isArray(taskRecord.metadata) &&
|
||||
typeof (taskRecord.metadata as { sourceRowId?: unknown }).sourceRowId === 'number'
|
||||
) {
|
||||
return (taskRecord.metadata as { sourceRowId: number }).sourceRowId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const runningTasks = (record: NormalizedFlowRecord | null) => {
|
||||
const collapsedTasks =
|
||||
record?.taskRecords.reduce((map, taskRecord) => {
|
||||
const current = map.get(taskRecord.activityId)
|
||||
if (!current || getTaskRowId(taskRecord) > getTaskRowId(current)) {
|
||||
map.set(taskRecord.activityId, taskRecord)
|
||||
}
|
||||
return map
|
||||
}, new Map<string, NormalizedTaskRecord>()) ?? new Map<string, NormalizedTaskRecord>()
|
||||
|
||||
const tasks = Array.from(collapsedTasks.values()).filter(
|
||||
(taskRecord) => taskRecord.status === 'RUNNING',
|
||||
)
|
||||
if (tasks.length <= 1) return tasks
|
||||
|
||||
const maxSequence = Math.max(...tasks.map(getTaskSequence))
|
||||
return tasks.filter((taskRecord) => getTaskSequence(taskRecord) === maxSequence)
|
||||
}
|
||||
|
||||
if (reviewRecord && OA_BOARD_RUNNING_STATUSES.has(reviewRecord.status)) {
|
||||
return runningTasks(reviewRecord)
|
||||
@ -482,6 +527,14 @@ function pickActiveTasks(records: NormalizedFlowRecord[]) {
|
||||
if (pricingTasks.length > 0) return pricingTasks
|
||||
}
|
||||
|
||||
const fallbackTasks = [
|
||||
...runningTasks(orderRecord),
|
||||
...runningTasks(artRecord),
|
||||
...runningTasks(pricingRecord),
|
||||
...runningTasks(mainRecord),
|
||||
]
|
||||
if (fallbackTasks.length > 0) return fallbackTasks
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
@ -518,9 +571,19 @@ function buildTaskRecordsWithMappings(
|
||||
processName: string,
|
||||
mappingLookup: ActivityMappingLookup,
|
||||
): NormalizedTaskRecord[] {
|
||||
const taskMap = new Map<string, NormalizedTaskRecord>()
|
||||
const latestRowsByActivity = new Map<string, SourceTaskRow>()
|
||||
|
||||
rows.forEach((row) => {
|
||||
const activityId = normalizeText(row.activityId) ?? ''
|
||||
const current = latestRowsByActivity.get(activityId)
|
||||
if (!current || Number(row.id) > Number(current.id)) {
|
||||
latestRowsByActivity.set(activityId, row)
|
||||
}
|
||||
})
|
||||
|
||||
const taskMap = new Map<string, NormalizedTaskRecord>()
|
||||
|
||||
Array.from(latestRowsByActivity.values()).forEach((row) => {
|
||||
const startedAtSource = parseChinaDateTime(row.createTime)
|
||||
const finishedAtSource = parseChinaDateTime(row.finishTime)
|
||||
const processCode = normalizeText((row as RowDataPacket).processCode) ?? ''
|
||||
@ -560,8 +623,11 @@ function buildTaskRecordsWithMappings(
|
||||
? toJsonValue({
|
||||
sequence: mapped.sequence,
|
||||
mappedBy: 'default-activity-mapping',
|
||||
sourceRowId: row.id,
|
||||
})
|
||||
: null,
|
||||
: toJsonValue({
|
||||
sourceRowId: row.id,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
@ -757,7 +823,14 @@ function computeOverallStatus(records: NormalizedFlowRecord[]) {
|
||||
if (records.some((record) => OA_BOARD_RUNNING_STATUSES.has(record.status))) {
|
||||
return 'RUNNING'
|
||||
}
|
||||
if (records.some((record) => record.status === 'TERMINATED' || record.result === 'refuse')) {
|
||||
if (
|
||||
records.some(
|
||||
(record) => (normalizeText(record.result)?.toUpperCase() ?? '') === 'REFUSE',
|
||||
)
|
||||
) {
|
||||
return 'REFUSED'
|
||||
}
|
||||
if (records.some((record) => record.status === 'TERMINATED')) {
|
||||
return 'TERMINATED'
|
||||
}
|
||||
if (records.every((record) => OA_BOARD_COMPLETED_STATUSES.has(record.status))) {
|
||||
@ -766,6 +839,22 @@ function computeOverallStatus(records: NormalizedFlowRecord[]) {
|
||||
return records[0]?.status ?? null
|
||||
}
|
||||
|
||||
function computeOverallResult(records: NormalizedFlowRecord[]) {
|
||||
const normalizedResults = records
|
||||
.map((record) => normalizeText(record.result)?.toUpperCase() ?? '')
|
||||
.filter(Boolean)
|
||||
|
||||
if (normalizedResults.includes('REFUSE')) {
|
||||
return 'REFUSE'
|
||||
}
|
||||
|
||||
if (normalizedResults.includes('AGREE')) {
|
||||
return 'AGREE'
|
||||
}
|
||||
|
||||
return normalizeText(records[0]?.result) ?? null
|
||||
}
|
||||
|
||||
type TimelineMetricDefinition = {
|
||||
key: string
|
||||
label: string
|
||||
@ -1071,18 +1160,13 @@ function buildProjectGraph(group: PreparedProjectGroup): OaBoardProjectGraphInpu
|
||||
|
||||
const reviewRecord = records.find((record) => record.flowType === 'review') ?? null
|
||||
const orderRecord = records.find((record) => record.flowType === 'order') ?? null
|
||||
const artRecord = records.find((record) => record.flowType === 'art') ?? null
|
||||
const pricingRecord = records.find((record) => record.flowType === 'pricing') ?? null
|
||||
const mainRecord = records.find((record) => record.flowType === 'main') ?? null
|
||||
const currentFlow = pickCurrentFlow(records)
|
||||
const activeTasks = pickActiveTasks(records)
|
||||
|
||||
const productType =
|
||||
reviewRecord?.productType ??
|
||||
orderRecord?.productType ??
|
||||
pricingRecord?.productType ??
|
||||
artRecord?.productType ??
|
||||
null
|
||||
const productType = reviewRecord?.productType ?? null
|
||||
const overallStatus = computeOverallStatus(records)
|
||||
const overallResult = computeOverallResult(records)
|
||||
|
||||
const overallActualHours = computeOverallActualHours(records)
|
||||
const overallTargetHours = getOaBoardProductTargetHours(productType)
|
||||
@ -1100,7 +1184,7 @@ function buildProjectGraph(group: PreparedProjectGroup): OaBoardProjectGraphInpu
|
||||
}, null)
|
||||
|
||||
const runningTask =
|
||||
currentFlow?.taskRecords.find((taskRecord) => taskRecord.status === 'RUNNING') ?? null
|
||||
pickActiveTasks(currentFlow ? [currentFlow] : [])[0] ?? null
|
||||
|
||||
const reviewFieldSnapshots = reviewRecord?.fieldSnapshots ?? []
|
||||
const styleImages = toImageList(readFieldSnapshot(reviewFieldSnapshots, ['款式图片']))
|
||||
@ -1118,7 +1202,7 @@ function buildProjectGraph(group: PreparedProjectGroup): OaBoardProjectGraphInpu
|
||||
const formSummary = {
|
||||
styleImageLabel: toImageCountLabel(readFieldSnapshot(reviewFieldSnapshots, ['款式图片'])),
|
||||
openingCode: toDisplayText(readFieldSnapshot(reviewFieldSnapshots, ['开款编号'])),
|
||||
totalFlowStatusLabel: toFlowStatusLabel(mainRecord?.status ?? reviewRecord?.status, mainRecord?.result ?? reviewRecord?.result),
|
||||
totalFlowStatusLabel: toFlowStatusLabel(overallStatus, overallResult),
|
||||
approvalStageDisplay: joinUnique(activeTasks.map((taskRecord) => taskRecord.stageName)),
|
||||
currentOwnerDisplay: joinUnique(
|
||||
activeTasks.map((taskRecord) => taskRecord.ownerLabel || taskRecord.userId || null),
|
||||
@ -1161,86 +1245,26 @@ function buildProjectGraph(group: PreparedProjectGroup): OaBoardProjectGraphInpu
|
||||
boardKey: group.boardKey,
|
||||
boardKeyType: group.boardKeyType,
|
||||
primaryFlowType: reviewRecord?.flowType ?? records[0]?.flowType ?? 'review',
|
||||
title:
|
||||
reviewRecord?.title ??
|
||||
orderRecord?.title ??
|
||||
artRecord?.title ??
|
||||
pricingRecord?.title ??
|
||||
mainRecord?.title ??
|
||||
group.boardKey,
|
||||
productName:
|
||||
reviewRecord?.productName ??
|
||||
orderRecord?.productName ??
|
||||
artRecord?.productName ??
|
||||
pricingRecord?.productName ??
|
||||
null,
|
||||
title: reviewRecord?.title ?? group.boardKey,
|
||||
productName: reviewRecord?.productName ?? null,
|
||||
productType,
|
||||
brand:
|
||||
reviewRecord?.brand ??
|
||||
orderRecord?.brand ??
|
||||
artRecord?.brand ??
|
||||
pricingRecord?.brand ??
|
||||
null,
|
||||
productCategory:
|
||||
reviewRecord?.productCategory ??
|
||||
orderRecord?.productCategory ??
|
||||
pricingRecord?.productCategory ??
|
||||
null,
|
||||
categoryName:
|
||||
reviewRecord?.categoryName ??
|
||||
orderRecord?.categoryName ??
|
||||
pricingRecord?.categoryName ??
|
||||
artRecord?.categoryName ??
|
||||
null,
|
||||
spu:
|
||||
reviewRecord?.spu ??
|
||||
orderRecord?.spu ??
|
||||
artRecord?.spu ??
|
||||
pricingRecord?.spu ??
|
||||
null,
|
||||
brand: reviewRecord?.brand ?? null,
|
||||
productCategory: reviewRecord?.productCategory ?? null,
|
||||
categoryName: reviewRecord?.categoryName ?? null,
|
||||
spu: reviewRecord?.spu ?? null,
|
||||
openingCode: reviewRecord?.openingCode ?? null,
|
||||
orderCode: orderRecord?.orderCode ?? null,
|
||||
pricingCode: pricingRecord?.pricingCode ?? null,
|
||||
sourceBusinessId:
|
||||
reviewRecord?.businessId ??
|
||||
orderRecord?.businessId ??
|
||||
pricingRecord?.businessId ??
|
||||
artRecord?.businessId ??
|
||||
mainRecord?.businessId ??
|
||||
null,
|
||||
originatorUserId:
|
||||
reviewRecord?.originatorUserId ??
|
||||
orderRecord?.originatorUserId ??
|
||||
artRecord?.originatorUserId ??
|
||||
pricingRecord?.originatorUserId ??
|
||||
mainRecord?.originatorUserId ??
|
||||
null,
|
||||
originatorDeptId:
|
||||
reviewRecord?.originatorDeptId ??
|
||||
orderRecord?.originatorDeptId ??
|
||||
artRecord?.originatorDeptId ??
|
||||
pricingRecord?.originatorDeptId ??
|
||||
mainRecord?.originatorDeptId ??
|
||||
null,
|
||||
originatorDeptName:
|
||||
reviewRecord?.originatorDeptName ??
|
||||
orderRecord?.originatorDeptName ??
|
||||
artRecord?.originatorDeptName ??
|
||||
pricingRecord?.originatorDeptName ??
|
||||
mainRecord?.originatorDeptName ??
|
||||
null,
|
||||
sourceBusinessId: reviewRecord?.businessId ?? null,
|
||||
originatorUserId: reviewRecord?.originatorUserId ?? null,
|
||||
originatorDeptId: reviewRecord?.originatorDeptId ?? null,
|
||||
originatorDeptName: reviewRecord?.originatorDeptName ?? null,
|
||||
currentStageKey: runningTask?.timelineStageKey ?? currentFlow?.flowType ?? null,
|
||||
currentStageName: runningTask?.timelineStageName ?? currentFlow?.processName ?? null,
|
||||
currentApprovalStageKey: runningTask?.stageKey ?? null,
|
||||
currentApprovalStageName: joinUnique(activeTasks.map((taskRecord) => taskRecord.stageName)) || null,
|
||||
overallStatus: computeOverallStatus(records),
|
||||
overallResult:
|
||||
pricingRecord?.result ??
|
||||
orderRecord?.result ??
|
||||
artRecord?.result ??
|
||||
reviewRecord?.result ??
|
||||
mainRecord?.result ??
|
||||
null,
|
||||
overallStatus,
|
||||
overallResult,
|
||||
overallTargetHours,
|
||||
overallActualHours,
|
||||
overallTimeoutHours,
|
||||
@ -1315,20 +1339,147 @@ async function fetchSourceData(limit?: number): Promise<NormalizedFlowRecord[]>
|
||||
|
||||
try {
|
||||
const processCodes = Object.values(OA_BOARD_PROCESS_CODES)
|
||||
const reviewProcessCode = OA_BOARD_PROCESS_CODES.review
|
||||
const workflowLimitSql = limit ? `LIMIT ${Math.max(1, Math.floor(limit))}` : ''
|
||||
let workflowRows: SourceWorkflowRow[] = []
|
||||
|
||||
const [workflowRows] = await connection.query<SourceWorkflowRow[]>(
|
||||
`
|
||||
SELECT finishTime, businessId, title, result, originatorUserId, originatorDeptId,
|
||||
originatorDeptName, status, bizAction, processInstanceId, joinAt,
|
||||
updateAt, createAt, processCode, flowType
|
||||
FROM ding_Workflow
|
||||
WHERE processCode IN (${processCodes.map(() => '?').join(', ')})
|
||||
ORDER BY updateAt DESC
|
||||
${workflowLimitSql}
|
||||
`,
|
||||
processCodes,
|
||||
)
|
||||
if (limit) {
|
||||
const [reviewWorkflowRows] = await connection.query<SourceWorkflowRow[]>(
|
||||
`
|
||||
SELECT finishTime, businessId, title, result, originatorUserId, originatorDeptId,
|
||||
originatorDeptName, status, bizAction, processInstanceId, joinAt,
|
||||
updateAt, createAt, processCode, flowType
|
||||
FROM ding_Workflow
|
||||
WHERE processCode = ?
|
||||
ORDER BY updateAt DESC
|
||||
${workflowLimitSql}
|
||||
`,
|
||||
[reviewProcessCode],
|
||||
)
|
||||
|
||||
const reviewProcessInstanceIds = reviewWorkflowRows.map((row) => row.processInstanceId)
|
||||
const [reviewSchemeRows] =
|
||||
reviewProcessInstanceIds.length > 0
|
||||
? await connection.query<SourceSchemeRow[]>(
|
||||
`
|
||||
SELECT processInstanceId, componentType, name, CAST(value AS CHAR) AS value, CAST(extValue AS CHAR) AS extValue
|
||||
FROM ding_WorkflowScheme
|
||||
WHERE processInstanceId IN (${reviewProcessInstanceIds.map(() => '?').join(', ')})
|
||||
`,
|
||||
reviewProcessInstanceIds,
|
||||
)
|
||||
: [[] as SourceSchemeRow[]]
|
||||
|
||||
const openingCodes = Array.from(
|
||||
new Set(
|
||||
reviewSchemeRows
|
||||
.filter((row) => OA_BOARD_JOIN_FIELD_NAMES.openingCode.includes(String(row.name ?? '').trim()))
|
||||
.map((row) => normalizeText(row.value))
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
),
|
||||
)
|
||||
|
||||
const linkedFieldNames = Array.from(
|
||||
new Set([
|
||||
...OA_BOARD_JOIN_FIELD_NAMES.openingCode,
|
||||
...OA_BOARD_JOIN_FIELD_NAMES.orderCode,
|
||||
...OA_BOARD_JOIN_FIELD_NAMES.pricingCode,
|
||||
...OA_BOARD_JOIN_FIELD_NAMES.artCode,
|
||||
]),
|
||||
)
|
||||
|
||||
const otherProcessCodes = processCodes.filter((code) => code !== reviewProcessCode)
|
||||
const [linkedWorkflowRows] =
|
||||
openingCodes.length > 0
|
||||
? await connection.query<SourceWorkflowRow[]>(
|
||||
`
|
||||
SELECT DISTINCT w.finishTime, w.businessId, w.title, w.result, w.originatorUserId, w.originatorDeptId,
|
||||
w.originatorDeptName, w.status, w.bizAction, w.processInstanceId, w.joinAt,
|
||||
w.updateAt, w.createAt, w.processCode, w.flowType
|
||||
FROM ding_Workflow w
|
||||
JOIN ding_WorkflowScheme s
|
||||
ON s.processInstanceId = w.processInstanceId
|
||||
WHERE w.processCode IN (${otherProcessCodes.map(() => '?').join(', ')})
|
||||
AND s.name IN (${linkedFieldNames.map(() => '?').join(', ')})
|
||||
AND CAST(s.value AS CHAR) IN (${openingCodes.map(() => '?').join(', ')})
|
||||
ORDER BY w.updateAt DESC
|
||||
`,
|
||||
[...otherProcessCodes, ...linkedFieldNames, ...openingCodes],
|
||||
)
|
||||
: [[] as SourceWorkflowRow[]]
|
||||
|
||||
const workflowMap = new Map<string, SourceWorkflowRow>()
|
||||
reviewWorkflowRows.forEach((row) => workflowMap.set(row.processInstanceId, row))
|
||||
linkedWorkflowRows.forEach((row) => workflowMap.set(row.processInstanceId, row))
|
||||
workflowRows = Array.from(workflowMap.values())
|
||||
} else {
|
||||
const [reviewWorkflowRows] = await connection.query<SourceWorkflowRow[]>(
|
||||
`
|
||||
SELECT finishTime, businessId, title, result, originatorUserId, originatorDeptId,
|
||||
originatorDeptName, status, bizAction, processInstanceId, joinAt,
|
||||
updateAt, createAt, processCode, flowType
|
||||
FROM ding_Workflow
|
||||
WHERE processCode = ?
|
||||
ORDER BY updateAt DESC
|
||||
`,
|
||||
[reviewProcessCode],
|
||||
)
|
||||
|
||||
const reviewProcessInstanceIds = reviewWorkflowRows.map((row) => row.processInstanceId)
|
||||
const [reviewSchemeRows] =
|
||||
reviewProcessInstanceIds.length > 0
|
||||
? await connection.query<SourceSchemeRow[]>(
|
||||
`
|
||||
SELECT processInstanceId, componentType, name, CAST(value AS CHAR) AS value, CAST(extValue AS CHAR) AS extValue
|
||||
FROM ding_WorkflowScheme
|
||||
WHERE processInstanceId IN (${reviewProcessInstanceIds.map(() => '?').join(', ')})
|
||||
`,
|
||||
reviewProcessInstanceIds,
|
||||
)
|
||||
: [[] as SourceSchemeRow[]]
|
||||
|
||||
const openingCodes = Array.from(
|
||||
new Set(
|
||||
reviewSchemeRows
|
||||
.filter((row) => OA_BOARD_JOIN_FIELD_NAMES.openingCode.includes(String(row.name ?? '').trim()))
|
||||
.map((row) => normalizeText(row.value))
|
||||
.filter((value): value is string => Boolean(value)),
|
||||
),
|
||||
)
|
||||
|
||||
const linkedFieldNames = Array.from(
|
||||
new Set([
|
||||
...OA_BOARD_JOIN_FIELD_NAMES.orderCode,
|
||||
...OA_BOARD_JOIN_FIELD_NAMES.pricingCode,
|
||||
...OA_BOARD_JOIN_FIELD_NAMES.artCode,
|
||||
]),
|
||||
)
|
||||
|
||||
const otherProcessCodes = processCodes.filter((code) => code !== reviewProcessCode)
|
||||
const [linkedWorkflowRows] =
|
||||
openingCodes.length > 0
|
||||
? await connection.query<SourceWorkflowRow[]>(
|
||||
`
|
||||
SELECT DISTINCT w.finishTime, w.businessId, w.title, w.result, w.originatorUserId, w.originatorDeptId,
|
||||
w.originatorDeptName, w.status, w.bizAction, w.processInstanceId, w.joinAt,
|
||||
w.updateAt, w.createAt, w.processCode, w.flowType
|
||||
FROM ding_Workflow w
|
||||
JOIN ding_WorkflowScheme s
|
||||
ON s.processInstanceId = w.processInstanceId
|
||||
WHERE w.processCode IN (${otherProcessCodes.map(() => '?').join(', ')})
|
||||
AND s.name IN (${linkedFieldNames.map(() => '?').join(', ')})
|
||||
AND CAST(s.value AS CHAR) IN (${openingCodes.map(() => '?').join(', ')})
|
||||
ORDER BY w.updateAt DESC
|
||||
`,
|
||||
[...otherProcessCodes, ...linkedFieldNames, ...openingCodes],
|
||||
)
|
||||
: [[] as SourceWorkflowRow[]]
|
||||
|
||||
const workflowMap = new Map<string, SourceWorkflowRow>()
|
||||
reviewWorkflowRows.forEach((row) => workflowMap.set(row.processInstanceId, row))
|
||||
linkedWorkflowRows.forEach((row) => workflowMap.set(row.processInstanceId, row))
|
||||
workflowRows = Array.from(workflowMap.values())
|
||||
}
|
||||
|
||||
const processInstanceIds = Array.from(
|
||||
new Set(
|
||||
@ -1353,7 +1504,7 @@ async function fetchSourceData(limit?: number): Promise<NormalizedFlowRecord[]>
|
||||
|
||||
const [taskRows] = await connection.query<SourceTaskRow[]>(
|
||||
`
|
||||
SELECT t.processInstanceId, t.result, t.activityId, t.finishTime, t.createTime, t.userId, t.taskId, t.status,
|
||||
SELECT t.id, t.processInstanceId, t.result, t.activityId, t.finishTime, t.createTime, t.userId, t.taskId, t.status,
|
||||
w.processCode AS processCode
|
||||
FROM ding_WorkflowTask t
|
||||
JOIN ding_Workflow w
|
||||
|
||||
@ -26,8 +26,8 @@ export const OA_BOARD_WORKBOOK_FLOW_NAME_MAP = {
|
||||
export const OA_BOARD_FLOW_PRIORITY: OaBoardFlowType[] = [
|
||||
'review',
|
||||
'order',
|
||||
'pricing',
|
||||
'art',
|
||||
'pricing',
|
||||
'main',
|
||||
]
|
||||
|
||||
@ -534,7 +534,7 @@ export const OA_BOARD_JOIN_FIELD_NAMES = {
|
||||
openingCode: ['开款编号'],
|
||||
orderCode: ['新品首单下单号'],
|
||||
pricingCode: ['品牌限价单号'],
|
||||
artCode: ['视觉出图单号'],
|
||||
artCode: ['视觉出图单号', '出图单号'],
|
||||
spu: ['SPU'],
|
||||
productType: ['产品类型'],
|
||||
brand: ['品牌', '开款品牌'],
|
||||
@ -547,4 +547,6 @@ export const OA_BOARD_RUNNING_STATUSES = new Set(['RUNNING', 'PROCESSING', 'PEND
|
||||
|
||||
export const OA_BOARD_COMPLETED_STATUSES = new Set(['COMPLETED', 'TERMINATED'])
|
||||
|
||||
export const OA_BOARD_DEFAULT_TRACKED_STATUSES = ['RUNNING', 'COMPLETED'] as const
|
||||
|
||||
export const OA_BOARD_PARALLEL_FLOW_TYPES = new Set<OaBoardFlowType>(['order', 'art'])
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import {
|
||||
getOaBoardProductTargetHours,
|
||||
OA_BOARD_DEFAULT_TRACKED_STATUSES,
|
||||
OA_BOARD_FLOW_LABELS,
|
||||
OA_BOARD_FLOW_PRIORITY,
|
||||
OA_BOARD_STAGE_DEFINITIONS,
|
||||
@ -10,9 +11,16 @@ import { queryOaBoardPreview } from './oa-board-sync.service'
|
||||
export interface OaBoardProjectListFilter {
|
||||
keyword?: string
|
||||
approvalNumber?: string
|
||||
overallStatus?: string
|
||||
overallStatus?: string[]
|
||||
createdDateStart?: string
|
||||
createdDateEnd?: string
|
||||
productType?: string
|
||||
approvalStage?: string
|
||||
productPlanner?: string
|
||||
structureDesigner?: string
|
||||
appearanceDesigner?: string
|
||||
purchaseOwner?: string
|
||||
spuOwner?: string
|
||||
owner?: string
|
||||
nodeName?: string
|
||||
timeoutOnly?: boolean
|
||||
@ -142,6 +150,24 @@ export interface OaBoardSummaryDTO {
|
||||
productTypes: string[]
|
||||
}
|
||||
|
||||
function countFilledFormSummaryFields(item: OaBoardProjectListItemDTO) {
|
||||
return Object.values(item.formSummary).filter((value) => {
|
||||
if (typeof value !== 'string') return Boolean(value)
|
||||
const normalized = value.trim()
|
||||
return normalized !== '' && normalized !== '-'
|
||||
}).length
|
||||
}
|
||||
|
||||
function toDateOnly(value: string | null | undefined) {
|
||||
const text = String(value ?? '').trim()
|
||||
return text ? text.slice(0, 10) : ''
|
||||
}
|
||||
|
||||
function parseDateSortValue(value: string | null | undefined) {
|
||||
const dateValue = value ? new Date(value).getTime() : Number.NaN
|
||||
return Number.isFinite(dateValue) ? dateValue : 0
|
||||
}
|
||||
|
||||
export interface OaBoardStageMatrixRowDTO {
|
||||
productType: string
|
||||
totalTargetHours: number
|
||||
@ -263,6 +289,305 @@ export function createEmptyOaBoardDashboard(productTypes: string[] = []): OaBoar
|
||||
}
|
||||
}
|
||||
|
||||
function buildMockTimelineMetrics(input: {
|
||||
totalTargetDays: number
|
||||
totalActualDays: number
|
||||
exteriorActualDays: number
|
||||
structureActualDays: number
|
||||
samplePrepareActualDays: number
|
||||
sampleReviewActualDays: number
|
||||
productionActualDays: number
|
||||
artActualDays: number
|
||||
pricingActualDays: number
|
||||
listingActualDays: number
|
||||
}): OaBoardTimelineMetricDTO[] {
|
||||
return [
|
||||
{
|
||||
key: 'total_actual',
|
||||
label: '总实际时效',
|
||||
targetDays: input.totalTargetDays,
|
||||
actualDays: input.totalActualDays,
|
||||
notes:
|
||||
input.totalActualDays > input.totalTargetDays
|
||||
? '人工验证数据:总实际时效已超过目标时效。'
|
||||
: '人工验证数据:总实际时效在目标范围内。',
|
||||
},
|
||||
{ key: 'exterior', label: '外观', targetDays: 3, actualDays: input.exteriorActualDays },
|
||||
{ key: 'structure', label: '结构', targetDays: 2, actualDays: input.structureActualDays },
|
||||
{ key: 'sample_prepare', label: '打样', targetDays: 3, actualDays: input.samplePrepareActualDays },
|
||||
{ key: 'sample_review', label: '审样', targetDays: 2, actualDays: input.sampleReviewActualDays },
|
||||
{ key: 'production', label: '生产', targetDays: 13, actualDays: input.productionActualDays },
|
||||
{ key: 'art', label: '出图', targetDays: 4, actualDays: input.artActualDays },
|
||||
{ key: 'pricing_flow', label: '限价', targetDays: 1, actualDays: input.pricingActualDays },
|
||||
{ key: 'listing', label: '运营上架', targetDays: 1, actualDays: input.listingActualDays },
|
||||
]
|
||||
}
|
||||
|
||||
function buildMockOaBoardItem(input: {
|
||||
id: string
|
||||
boardKey: string
|
||||
title: string
|
||||
productName: string
|
||||
productType: string
|
||||
spu: string
|
||||
salesBrands: string
|
||||
plannedSalesBrands: string
|
||||
openingCode: string
|
||||
approvalStage: string
|
||||
currentNode: string
|
||||
currentOwner: string
|
||||
overallStatus: 'RUNNING' | 'COMPLETED'
|
||||
productPlanner: string
|
||||
structureDesigner: string
|
||||
appearanceDesigner: string
|
||||
purchaseOwner: string
|
||||
spuOwner: string
|
||||
createdDate: string
|
||||
finishedDate: string | null
|
||||
totalTargetDays: number
|
||||
totalActualDays: number
|
||||
timelineMetrics: OaBoardTimelineMetricDTO[]
|
||||
}): OaBoardProjectListItemDTO {
|
||||
const timeoutHours = Math.max(input.totalActualDays - input.totalTargetDays, 0)
|
||||
|
||||
return {
|
||||
id: input.id,
|
||||
boardKey: input.boardKey,
|
||||
boardKeyType: 'MANUAL_SAMPLE',
|
||||
title: input.title,
|
||||
productName: input.productName,
|
||||
productType: input.productType,
|
||||
brand: input.salesBrands,
|
||||
productCategory: '成品',
|
||||
categoryName: '柜类',
|
||||
spu: input.spu,
|
||||
openingCode: input.openingCode,
|
||||
orderCode: null,
|
||||
pricingCode: null,
|
||||
currentStageKey: null,
|
||||
currentStageName: input.approvalStage,
|
||||
currentApprovalStageKey: null,
|
||||
currentApprovalStageName: input.approvalStage,
|
||||
overallStatus: input.overallStatus,
|
||||
overallResult: input.overallStatus === 'COMPLETED' ? 'agree' : null,
|
||||
overallTargetHours: input.totalTargetDays,
|
||||
overallActualHours: input.totalActualDays,
|
||||
overallTimeoutHours: timeoutHours > 0 ? Number(timeoutHours.toFixed(2)) : 0,
|
||||
overallTimeoutFlag: timeoutHours > 0,
|
||||
createdDate: input.createdDate,
|
||||
finishedDate: input.finishedDate,
|
||||
originatorDeptName: '产品开发中心',
|
||||
lastSourceUpdatedAt: new Date().toISOString(),
|
||||
styleImages: [],
|
||||
formSummary: {
|
||||
styleImageLabel: '',
|
||||
openingCode: input.openingCode,
|
||||
totalFlowStatusLabel: input.overallStatus === 'COMPLETED' ? '已完成' : '进行中',
|
||||
approvalStageDisplay: input.approvalStage,
|
||||
currentOwnerDisplay: input.currentOwner,
|
||||
currentNodeDisplay: input.currentNode,
|
||||
demandType: '新品开发',
|
||||
developmentPlan: '常规开发',
|
||||
productType: input.productType,
|
||||
shippingType: '整柜',
|
||||
needVisualDesign: '需要',
|
||||
plannedSalesBrands: input.plannedSalesBrands,
|
||||
category: '成品',
|
||||
categoryName: '柜类',
|
||||
salesPriceBand: '1000-1500',
|
||||
platformPriceBand: '899-1399',
|
||||
competitorLink: 'https://example.com/competitor',
|
||||
openingReason: '人工验证数据:用于本地校验流程明细、筛选和进度展示。',
|
||||
isTestStyle: '否',
|
||||
testStyleCode: '',
|
||||
productPlanner: input.productPlanner,
|
||||
operationBrandOwner: '何运营',
|
||||
spuOwner: input.spuOwner,
|
||||
appearanceDesigner: input.appearanceDesigner,
|
||||
structureDesigner: input.structureDesigner,
|
||||
purchaseOwner: input.purchaseOwner,
|
||||
merchandiserOwner: '周跟单',
|
||||
pricingOwner: '郑限价',
|
||||
workflowLink: 'https://example.com/workflow',
|
||||
needPatent: '否',
|
||||
needTrademark: '否',
|
||||
reviewResult: input.overallStatus === 'COMPLETED' ? '通过' : '待评审',
|
||||
sampleReviewResult: input.overallStatus === 'COMPLETED' ? '通过' : '待审样',
|
||||
salesBrands: input.salesBrands,
|
||||
spu: input.spu,
|
||||
openingBrand: '由壹点',
|
||||
},
|
||||
flowChips: [],
|
||||
nodeAssessments: [],
|
||||
timelineMetrics: input.timelineMetrics,
|
||||
}
|
||||
}
|
||||
|
||||
export function createMockOaBoardDashboard(): OaBoardDashboardDTO {
|
||||
const items: OaBoardProjectListItemDTO[] = [
|
||||
buildMockOaBoardItem({
|
||||
id: 'mock-oa-001',
|
||||
boardKey: 'OA-MOCK-202606-001',
|
||||
title: '奶油白餐边柜',
|
||||
productName: '奶油白餐边柜',
|
||||
productType: '成品开发-板式/钢木/铝制(无需磨具)',
|
||||
spu: 'SPU-CBG-001',
|
||||
salesBrands: '由壹点',
|
||||
plannedSalesBrands: '由壹点 / 卓禾',
|
||||
openingCode: 'KKCBG-001',
|
||||
approvalStage: '结构设计',
|
||||
currentNode: '结构设计',
|
||||
currentOwner: '王结构',
|
||||
overallStatus: 'RUNNING',
|
||||
productPlanner: '李企划',
|
||||
structureDesigner: '王结构',
|
||||
appearanceDesigner: '张外观',
|
||||
purchaseOwner: '陈采购',
|
||||
spuOwner: '孙SPU',
|
||||
createdDate: '2026-06-14T09:20:00+08:00',
|
||||
finishedDate: null,
|
||||
totalTargetDays: 23,
|
||||
totalActualDays: 9,
|
||||
timelineMetrics: buildMockTimelineMetrics({
|
||||
totalTargetDays: 23,
|
||||
totalActualDays: 9,
|
||||
exteriorActualDays: 2,
|
||||
structureActualDays: 3,
|
||||
samplePrepareActualDays: 1,
|
||||
sampleReviewActualDays: 0,
|
||||
productionActualDays: 0,
|
||||
artActualDays: 3,
|
||||
pricingActualDays: 0,
|
||||
listingActualDays: 0,
|
||||
}),
|
||||
}),
|
||||
buildMockOaBoardItem({
|
||||
id: 'mock-oa-002',
|
||||
boardKey: 'OA-MOCK-202606-002',
|
||||
title: '胡桃色电视柜',
|
||||
productName: '胡桃色电视柜',
|
||||
productType: '成品开发-实木',
|
||||
spu: 'SPU-TV-018',
|
||||
salesBrands: '卓禾',
|
||||
plannedSalesBrands: '卓禾',
|
||||
openingCode: 'HTDSG-018',
|
||||
approvalStage: '样品评审',
|
||||
currentNode: '样品评审',
|
||||
currentOwner: '赵企划',
|
||||
overallStatus: 'RUNNING',
|
||||
productPlanner: '赵企划',
|
||||
structureDesigner: '刘结构',
|
||||
appearanceDesigner: '林外观',
|
||||
purchaseOwner: '杨采购',
|
||||
spuOwner: '钱SPU',
|
||||
createdDate: '2026-06-10T14:00:00+08:00',
|
||||
finishedDate: null,
|
||||
totalTargetDays: 47,
|
||||
totalActualDays: 19,
|
||||
timelineMetrics: buildMockTimelineMetrics({
|
||||
totalTargetDays: 47,
|
||||
totalActualDays: 19,
|
||||
exteriorActualDays: 3,
|
||||
structureActualDays: 4,
|
||||
samplePrepareActualDays: 6,
|
||||
sampleReviewActualDays: 2,
|
||||
productionActualDays: 0,
|
||||
artActualDays: 4,
|
||||
pricingActualDays: 0,
|
||||
listingActualDays: 0,
|
||||
}),
|
||||
}),
|
||||
buildMockOaBoardItem({
|
||||
id: 'mock-oa-003',
|
||||
boardKey: 'OA-MOCK-202606-003',
|
||||
title: '轻奢床头柜',
|
||||
productName: '轻奢床头柜',
|
||||
productType: '部件开发-纸箱/泡沫',
|
||||
spu: 'SPU-BG-009',
|
||||
salesBrands: '由壹点',
|
||||
plannedSalesBrands: '由壹点 / 京喜',
|
||||
openingCode: 'QSCTG-009',
|
||||
approvalStage: '运营上架',
|
||||
currentNode: '运营上架',
|
||||
currentOwner: '韩运营',
|
||||
overallStatus: 'COMPLETED',
|
||||
productPlanner: '周企划',
|
||||
structureDesigner: '郭结构',
|
||||
appearanceDesigner: '宋外观',
|
||||
purchaseOwner: '许采购',
|
||||
spuOwner: '吴SPU',
|
||||
createdDate: '2026-06-01T10:00:00+08:00',
|
||||
finishedDate: '2026-06-16T18:00:00+08:00',
|
||||
totalTargetDays: 19,
|
||||
totalActualDays: 21,
|
||||
timelineMetrics: buildMockTimelineMetrics({
|
||||
totalTargetDays: 19,
|
||||
totalActualDays: 21,
|
||||
exteriorActualDays: 2,
|
||||
structureActualDays: 2,
|
||||
samplePrepareActualDays: 5,
|
||||
sampleReviewActualDays: 2,
|
||||
productionActualDays: 7,
|
||||
artActualDays: 4,
|
||||
pricingActualDays: 1,
|
||||
listingActualDays: 1,
|
||||
}),
|
||||
}),
|
||||
buildMockOaBoardItem({
|
||||
id: 'mock-oa-004',
|
||||
boardKey: 'OA-MOCK-202606-004',
|
||||
title: '岩板茶几',
|
||||
productName: '岩板茶几',
|
||||
productType: '成品开发-钢木/铝制(需磨具)',
|
||||
spu: 'SPU-CJ-032',
|
||||
salesBrands: '卓禾',
|
||||
plannedSalesBrands: '卓禾 / 京东',
|
||||
openingCode: 'YBCJ-032',
|
||||
approvalStage: '已归档',
|
||||
currentNode: '流程归档',
|
||||
currentOwner: '-',
|
||||
overallStatus: 'COMPLETED',
|
||||
productPlanner: '冯企划',
|
||||
structureDesigner: '郑结构',
|
||||
appearanceDesigner: '苏外观',
|
||||
purchaseOwner: '褚采购',
|
||||
spuOwner: '卫SPU',
|
||||
createdDate: '2026-05-28T11:30:00+08:00',
|
||||
finishedDate: '2026-06-12T16:30:00+08:00',
|
||||
totalTargetDays: 48,
|
||||
totalActualDays: 32,
|
||||
timelineMetrics: buildMockTimelineMetrics({
|
||||
totalTargetDays: 48,
|
||||
totalActualDays: 32,
|
||||
exteriorActualDays: 2,
|
||||
structureActualDays: 2,
|
||||
samplePrepareActualDays: 6,
|
||||
sampleReviewActualDays: 2,
|
||||
productionActualDays: 15,
|
||||
artActualDays: 4,
|
||||
pricingActualDays: 1,
|
||||
listingActualDays: 0,
|
||||
}),
|
||||
}),
|
||||
]
|
||||
|
||||
const productTypes = Array.from(
|
||||
new Set(items.map((item) => item.productType).filter((value): value is string => Boolean(value))),
|
||||
).sort((left, right) => left.localeCompare(right, 'zh-CN'))
|
||||
|
||||
return {
|
||||
items,
|
||||
summary: {
|
||||
totalProjects: items.length,
|
||||
timeoutProjects: items.filter((item) => item.overallTimeoutFlag).length,
|
||||
runningProjects: items.filter((item) => item.overallStatus === 'RUNNING').length,
|
||||
completedProjects: items.filter((item) => item.overallStatus === 'COMPLETED').length,
|
||||
productTypes,
|
||||
},
|
||||
stageMatrix: buildStageMatrixRows(),
|
||||
}
|
||||
}
|
||||
|
||||
function toIsoString(value: Date | null | undefined) {
|
||||
return value ? value.toISOString() : null
|
||||
}
|
||||
@ -424,6 +749,7 @@ type PreviewProjectRecord = Awaited<ReturnType<typeof queryOaBoardPreview>>['pro
|
||||
|
||||
function toProjectListItem(record: PreviewProjectRecord): OaBoardProjectListItemDTO {
|
||||
const metadata = isRecord(record.metadata) ? record.metadata : null
|
||||
const reviewFlow = record.flowInstances.find((flowInstance) => flowInstance.flowType === 'review')
|
||||
const formSummary = readFormSummary(metadata?.formSummary)
|
||||
const styleImages = normalizeImageList(metadata?.styleImages)
|
||||
const timelineMetrics = Array.isArray(metadata?.timelineMetrics)
|
||||
@ -445,7 +771,12 @@ function toProjectListItem(record: PreviewProjectRecord): OaBoardProjectListItem
|
||||
...(notes ? { notes } : {}),
|
||||
}
|
||||
})
|
||||
.filter((item): item is OaBoardTimelineMetricDTO => item !== null && Boolean(item.key) && Boolean(item.label))
|
||||
.filter(
|
||||
(
|
||||
item,
|
||||
): item is NonNullable<typeof item> =>
|
||||
Boolean(item?.key) && Boolean(item?.label),
|
||||
)
|
||||
: []
|
||||
const nodeAssessments = record.flowInstances
|
||||
.flatMap((flowInstance) =>
|
||||
@ -507,8 +838,8 @@ function toProjectListItem(record: PreviewProjectRecord): OaBoardProjectListItem
|
||||
overallActualHours: toDays(record.overallActualHours),
|
||||
overallTimeoutHours: toDays(record.overallTimeoutHours),
|
||||
overallTimeoutFlag: record.overallTimeoutFlag,
|
||||
createdDate: toIsoString(record.flowInstances.find((flowInstance) => flowInstance.flowType === 'main')?.createdAtSource ?? record.flowInstances[0]?.createdAtSource),
|
||||
finishedDate: toIsoString(record.flowInstances.find((flowInstance) => flowInstance.flowType === 'main')?.finishedAtSource ?? record.flowInstances[0]?.finishedAtSource),
|
||||
createdDate: toIsoString(reviewFlow?.createdAtSource ?? record.flowInstances[0]?.createdAtSource),
|
||||
finishedDate: toIsoString(reviewFlow?.finishedAtSource ?? record.flowInstances[0]?.finishedAtSource),
|
||||
originatorDeptName: record.originatorDeptName,
|
||||
lastSourceUpdatedAt: toIsoString(record.lastSourceUpdatedAt),
|
||||
styleImages,
|
||||
@ -546,25 +877,45 @@ function buildStageMatrixRows(): OaBoardStageMatrixRowDTO[] {
|
||||
|
||||
export class OaBoardService {
|
||||
async getDashboard(filter: OaBoardProjectListFilter = {}): Promise<OaBoardDashboardDTO> {
|
||||
const pageLimit = Number(process.env.OA_BOARD_PAGE_LIMIT || '120')
|
||||
const preview = await queryOaBoardPreview(Number.isFinite(pageLimit) && pageLimit > 0 ? pageLimit : 120)
|
||||
const allItems = preview.projectGraphs.map(toProjectListItem)
|
||||
const pageLimitRaw = process.env.OA_BOARD_PAGE_LIMIT?.trim()
|
||||
const pageLimit = pageLimitRaw ? Number(pageLimitRaw) : undefined
|
||||
const preview = await queryOaBoardPreview(
|
||||
typeof pageLimit === 'number' && Number.isFinite(pageLimit) && pageLimit > 0
|
||||
? pageLimit
|
||||
: undefined,
|
||||
)
|
||||
const allItems = preview.projectGraphs
|
||||
.map(toProjectListItem)
|
||||
.filter((item) => {
|
||||
const filledFieldCount = countFilledFormSummaryFields(item)
|
||||
const hasOpeningContext = Boolean(item.formSummary.openingCode || item.openingCode)
|
||||
const hasActionableOwner = Boolean(
|
||||
item.formSummary.currentOwnerDisplay &&
|
||||
item.formSummary.currentOwnerDisplay.trim() !== '-' &&
|
||||
item.formSummary.currentOwnerDisplay.trim() !== '',
|
||||
)
|
||||
const isMainReviewBoard = item.boardKeyType === 'OPENING'
|
||||
|
||||
if (isMainReviewBoard) {
|
||||
return true
|
||||
}
|
||||
|
||||
return hasOpeningContext || hasActionableOwner || filledFieldCount >= 8
|
||||
})
|
||||
|
||||
const effectiveStatuses =
|
||||
filter.overallStatus && filter.overallStatus.length > 0
|
||||
? filter.overallStatus
|
||||
: [...OA_BOARD_DEFAULT_TRACKED_STATUSES]
|
||||
|
||||
const items = allItems.filter((item) => {
|
||||
if (filter.keyword?.trim()) {
|
||||
const keyword = filter.keyword.trim().toLowerCase()
|
||||
const haystack = [
|
||||
item.boardKey,
|
||||
item.title,
|
||||
item.productName,
|
||||
item.productType,
|
||||
item.brand,
|
||||
item.spu,
|
||||
item.openingCode,
|
||||
item.orderCode,
|
||||
item.pricingCode,
|
||||
item.formSummary.currentNodeDisplay,
|
||||
item.formSummary.currentOwnerDisplay,
|
||||
item.formSummary.spu,
|
||||
item.formSummary.salesBrands,
|
||||
item.formSummary.plannedSalesBrands,
|
||||
]
|
||||
.map((value) => String(value ?? '').toLowerCase())
|
||||
.join('\n')
|
||||
@ -576,14 +927,24 @@ export class OaBoardService {
|
||||
|
||||
if (
|
||||
filter.approvalNumber?.trim() &&
|
||||
!String(item.boardKey || '')
|
||||
!String(item.formSummary.openingCode || item.openingCode || '')
|
||||
.toLowerCase()
|
||||
.includes(filter.approvalNumber.trim().toLowerCase())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filter.overallStatus?.trim() && item.overallStatus !== filter.overallStatus.trim()) {
|
||||
if (!effectiveStatuses.includes(String(item.overallStatus ?? '').trim())) {
|
||||
return false
|
||||
}
|
||||
|
||||
const createdDateOnly = toDateOnly(item.createdDate)
|
||||
|
||||
if (filter.createdDateStart?.trim() && createdDateOnly < filter.createdDateStart.trim()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (filter.createdDateEnd?.trim() && createdDateOnly > filter.createdDateEnd.trim()) {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -593,9 +954,43 @@ export class OaBoardService {
|
||||
|
||||
if (
|
||||
filter.approvalStage?.trim() &&
|
||||
!String(item.formSummary.approvalStageDisplay || item.currentApprovalStageName || '')
|
||||
.toLowerCase()
|
||||
.includes(filter.approvalStage.trim().toLowerCase())
|
||||
String(item.formSummary.approvalStageDisplay || item.currentApprovalStageName || '') !==
|
||||
filter.approvalStage.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
filter.productPlanner?.trim() &&
|
||||
item.formSummary.productPlanner !== filter.productPlanner.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
filter.structureDesigner?.trim() &&
|
||||
item.formSummary.structureDesigner !== filter.structureDesigner.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
filter.appearanceDesigner?.trim() &&
|
||||
item.formSummary.appearanceDesigner !== filter.appearanceDesigner.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
filter.purchaseOwner?.trim() &&
|
||||
item.formSummary.purchaseOwner !== filter.purchaseOwner.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
filter.spuOwner?.trim() &&
|
||||
item.formSummary.spuOwner !== filter.spuOwner.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
@ -623,6 +1018,10 @@ export class OaBoardService {
|
||||
}
|
||||
|
||||
return true
|
||||
}).sort((left, right) => {
|
||||
const timeDiff = parseDateSortValue(right.createdDate) - parseDateSortValue(left.createdDate)
|
||||
if (timeDiff !== 0) return timeDiff
|
||||
return right.boardKey.localeCompare(left.boardKey, 'zh-CN')
|
||||
})
|
||||
|
||||
const productTypes = Array.from(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user