fix(oa-board): stabilize filter and stage sync

This commit is contained in:
jingchuan 2026-06-25 17:59:32 +08:00
parent 8cc0dc5218
commit 9c696e1947
4 changed files with 59 additions and 24 deletions

View File

@ -298,7 +298,7 @@ export async function GET(request: NextRequest) {
salesBrands: item.formSummary.salesBrands || '',
spu: item.formSummary.spu || item.spu || '',
openingBrand: item.formSummary.openingBrand || '',
creatorLabel: item.originatorDeptName || item.boardKeyType || '',
creatorLabel: item.originatorUserName || '',
overallTargetHours: item.overallTargetHours ?? '',
overallActualHours: item.overallActualHours ?? '',
isOvertime: item.overallTimeoutFlag ? '是' : '否',

View File

@ -274,8 +274,7 @@ function readMetricValue(
}
function readCreatorDisplay(item: OaBoardProjectListItemDTO) {
const initiator = item.nodeAssessments.find((node) => node.nodeName.includes('发起'))?.ownerLabel
return initiator || item.originatorDeptName || item.boardKeyType || '-'
return item.originatorUserName || '-'
}
function buildTimingDetailFields(item: OaBoardProjectListItemDTO): TimingDetailField[] {
@ -343,12 +342,12 @@ function FilterMultiSelect({
return (
<label className="flex flex-col gap-2">
<span className="text-[12px] font-apple-medium text-apple-label-secondary">{label}</span>
<details className="group relative">
<details className="group">
<summary className="flex h-11 cursor-pointer list-none items-center justify-between rounded-apple-lg border border-black/8 bg-white/80 px-3.5 text-sm text-apple-label-primary outline-none transition marker:hidden group-open:border-[color:var(--color-blue)] group-open:bg-white">
<span className={values.length === 0 ? 'text-apple-label-tertiary' : ''}>{summaryLabel}</span>
<span className="text-[11px] text-apple-label-secondary">{values.length > 0 ? `${values.length} 项` : '多选'}</span>
</summary>
<div className="absolute z-40 mt-2 w-full min-w-[220px] rounded-apple-xl border border-black/8 bg-white p-2 shadow-[0_24px_60px_rgba(15,23,42,0.12)]">
<div className="mt-2 w-full min-w-[220px] rounded-apple-xl border border-black/8 bg-white p-2 shadow-[0_24px_60px_rgba(15,23,42,0.12)]">
<div className="mb-2 flex items-center justify-between px-2 pt-1">
<span className="text-[11px] text-apple-label-secondary">可多选</span>
<button

View File

@ -554,19 +554,21 @@ function joinUnique(values: Array<string | null | undefined>) {
return items.join(';')
}
async function resolveActiveApproverNameLookup(groups: PreparedProjectGroup[]) {
async function resolveUserNameLookup(groups: PreparedProjectGroup[]) {
const userIds = Array.from(
new Set(
groups
.flatMap((group) => pickActiveTasks(group.flowRecords))
.map((taskRecord) => normalizeText(taskRecord.userId))
.flatMap((group) => [
...pickActiveTasks(group.flowRecords).map((taskRecord) => normalizeText(taskRecord.userId)),
...group.flowRecords.map((record) => normalizeText(record.originatorUserId)),
])
.filter((value): value is string => Boolean(value)),
),
)
const approverNameLookup = new Map<string, string>()
const userNameLookup = new Map<string, string>()
if (userIds.length === 0) {
return approverNameLookup
return userNameLookup
}
const [users, employees] = await Promise.all([
@ -595,19 +597,19 @@ async function resolveActiveApproverNameLookup(groups: PreparedProjectGroup[]) {
const key = normalizeText(user.dingtalkUserId)
const name = normalizeText(user.name)
if (key && name) {
approverNameLookup.set(key, name)
userNameLookup.set(key, name)
}
})
employees.forEach((employee) => {
const key = normalizeText(employee.dingtalkUserId)
const name = normalizeText(employee.name)
if (key && name && !approverNameLookup.has(key)) {
approverNameLookup.set(key, name)
if (key && name && !userNameLookup.has(key)) {
userNameLookup.set(key, name)
}
})
const unresolvedUserIds = userIds.filter((userId) => !approverNameLookup.has(userId))
const unresolvedUserIds = userIds.filter((userId) => !userNameLookup.has(userId))
const externalResults = await Promise.allSettled(
unresolvedUserIds.map(async (userId) => ({
userId,
@ -625,12 +627,12 @@ async function resolveActiveApproverNameLookup(groups: PreparedProjectGroup[]) {
? normalizeText(result.value.employee.name)
: null
if (key && employeeName && !approverNameLookup.has(key)) {
approverNameLookup.set(key, employeeName)
if (key && employeeName && !userNameLookup.has(key)) {
userNameLookup.set(key, employeeName)
}
})
return approverNameLookup
return userNameLookup
}
function buildFieldSnapshots(rows: SourceSchemeRow[]): NormalizedFieldSnapshot[] {
@ -657,9 +659,29 @@ function buildFieldSnapshots(rows: SourceSchemeRow[]): NormalizedFieldSnapshot[]
function getSourceTaskRowSortValue(row: SourceTaskRow) {
const finishedAt = parseChinaDateTime(row.finishTime)?.getTime() ?? 0
const createdAt = parseChinaDateTime(row.createTime)?.getTime() ?? 0
const normalizedStatus = normalizeText(row.status)?.toUpperCase() ?? ''
const normalizedResult = normalizeText(row.result)?.toUpperCase() ?? ''
const statusPriority =
OA_BOARD_RUNNING_STATUSES.has(normalizedStatus)
? 3
: normalizedStatus === 'COMPLETED'
? 2
: normalizedStatus === 'CANCELED' || normalizedStatus === 'TERMINATED'
? 1
: 0
const resultPriority =
normalizedResult === 'REDIRECTED'
? 2
: normalizedResult === 'NONE'
? 1
: normalizedResult === 'REFUSE'
? 0
: 1
return {
eventTime: Math.max(finishedAt, createdAt),
hasFinishedAt: finishedAt > 0 ? 1 : 0,
createdAt,
statusPriority,
resultPriority,
rowId: Number(row.id) || 0,
}
}
@ -671,8 +693,14 @@ function isNewerSourceTaskRow(nextRow: SourceTaskRow, currentRow: SourceTaskRow)
if (nextValue.eventTime !== currentValue.eventTime) {
return nextValue.eventTime > currentValue.eventTime
}
if (nextValue.hasFinishedAt !== currentValue.hasFinishedAt) {
return nextValue.hasFinishedAt > currentValue.hasFinishedAt
if (nextValue.createdAt !== currentValue.createdAt) {
return nextValue.createdAt > currentValue.createdAt
}
if (nextValue.statusPriority !== currentValue.statusPriority) {
return nextValue.statusPriority > currentValue.statusPriority
}
if (nextValue.resultPriority !== currentValue.resultPriority) {
return nextValue.resultPriority > currentValue.resultPriority
}
return nextValue.rowId > currentValue.rowId
}
@ -1402,7 +1430,7 @@ function buildTimelineMetrics(
function buildProjectGraph(
group: PreparedProjectGroup,
approverNameLookup: Map<string, string>,
userNameLookup: Map<string, string>,
): OaBoardProjectGraphInput {
const records = [...group.flowRecords].sort((left, right) => {
const leftPriority = OA_BOARD_FLOW_PRIORITY.indexOf(left.flowType)
@ -1460,7 +1488,7 @@ function buildProjectGraph(
currentOwnerDisplay: joinUnique(
activeTasks.map((taskRecord) => {
const userId = normalizeText(taskRecord.userId)
const approverName = userId ? approverNameLookup.get(userId) ?? null : null
const approverName = userId ? userNameLookup.get(userId) ?? null : null
return approverName || taskRecord.ownerLabel || null
}),
) || '-',
@ -1514,6 +1542,10 @@ function buildProjectGraph(
pricingCode: pricingRecord?.pricingCode ?? null,
sourceBusinessId: reviewRecord?.businessId ?? null,
originatorUserId: reviewRecord?.originatorUserId ?? null,
originatorUserName:
reviewRecord?.originatorUserId
? userNameLookup.get(reviewRecord.originatorUserId) ?? null
: null,
originatorDeptId: reviewRecord?.originatorDeptId ?? null,
originatorDeptName: reviewRecord?.originatorDeptName ?? null,
currentStageKey: runningTask?.timelineStageKey ?? currentFlow?.flowType ?? null,
@ -1901,8 +1933,8 @@ export async function queryOaBoardPreview(limit?: number): Promise<OaBoardPrevie
: undefined
const sourceRecords = await fetchSourceData(normalizedLimit)
const projectGroups = buildProjectGroups(sourceRecords)
const approverNameLookup = await resolveActiveApproverNameLookup(projectGroups)
const projectGraphs = projectGroups.map((group) => buildProjectGraph(group, approverNameLookup))
const userNameLookup = await resolveUserNameLookup(projectGroups)
const projectGraphs = projectGroups.map((group) => buildProjectGraph(group, userNameLookup))
const counts = countNestedRecords(projectGraphs)
return {

View File

@ -96,6 +96,7 @@ export interface OaBoardProjectListItemDTO {
overallTimeoutFlag: boolean
createdDate: string | null
finishedDate: string | null
originatorUserName: string | null
originatorDeptName: string | null
lastSourceUpdatedAt: string | null
styleImages: OaBoardFormPreviewImageDTO[]
@ -258,6 +259,7 @@ export interface OaBoardProjectGraphInput {
pricingCode: string | null
sourceBusinessId: string | null
originatorUserId: string | null
originatorUserName: string | null
originatorDeptId: string | null
originatorDeptName: string | null
currentStageKey: string | null
@ -376,6 +378,7 @@ function buildMockOaBoardItem(input: {
overallTimeoutFlag: timeoutHours > 0,
createdDate: input.createdDate,
finishedDate: input.finishedDate,
originatorUserName: '系统管理员',
originatorDeptName: '产品开发中心',
lastSourceUpdatedAt: new Date().toISOString(),
styleImages: [],
@ -840,6 +843,7 @@ function toProjectListItem(record: PreviewProjectRecord): OaBoardProjectListItem
overallTimeoutFlag: record.overallTimeoutFlag,
createdDate: toIsoString(reviewFlow?.createdAtSource ?? record.flowInstances[0]?.createdAtSource),
finishedDate: toIsoString(reviewFlow?.finishedAtSource ?? record.flowInstances[0]?.finishedAtSource),
originatorUserName: record.originatorUserName,
originatorDeptName: record.originatorDeptName,
lastSourceUpdatedAt: toIsoString(record.lastSourceUpdatedAt),
styleImages,