Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in / Register
Toggle navigation
P
problem_rectification
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Registry
Registry
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
David Yang
problem_rectification
Commits
3917767f
Commit
3917767f
authored
Jul 29, 2026
by
David Yang
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: 问题整改转发
parent
9b9085cc
Show whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
1124 additions
and
110 deletions
+1124
-110
manifest.json
public/manifest.json
+1
-1
app.js
src/app.js
+13
-0
index.jsx
src/components/record-workbench/index.jsx
+1
-0
useSubjectRecord.js
src/pages/RecordDetailDrawer/hooks/useSubjectRecord.js
+7
-2
index.jsx
src/pages/RecordDetailDrawer/index.jsx
+13
-2
Layout.jsx
src/pages/problem_rectification/Layout.jsx
+1
-1
index.jsx
src/pages/problem_rectification/dashboard/index.jsx
+13
-12
index.jsx
src/pages/problem_rectification/flow-table/index.jsx
+373
-12
index.jsx
src/pages/problem_rectification/form/index.jsx
+39
-11
index.jsx
src/pages/problem_rectification/list/index.jsx
+312
-8
problem-rectification.business-flow.md
...blem_rectification/problem-rectification.business-flow.md
+103
-29
problemRectificationDashboardService.js
...lem_rectification/problemRectificationDashboardService.js
+9
-15
problemRectificationFormService.js
.../problem_rectification/problemRectificationFormService.js
+28
-0
problemRectificationListService.js
.../problem_rectification/problemRectificationListService.js
+171
-14
subjectsDefinition.md
src/services/subjectsDefinition.md
+40
-3
No files found.
public/manifest.json
View file @
3917767f
{
"name"
:
"problem_rectification_new"
,
"version"
:
"1.0.
4
"
,
"version"
:
"1.0.
6
"
,
"build"
:
"abc"
,
"description"
:
"merge master"
,
"date"
:
"2025-07-14 19:27:20"
,
...
...
src/app.js
View file @
3917767f
...
...
@@ -2,6 +2,19 @@ import '@/global.less';
import
'@/tailwind.css'
;
import
{
defineApp
}
from
'umi'
;
const
LEGACY_FLOW_PATH
=
'/self_inspection_form'
;
const
appBasePath
=
String
(
PROBLEM_RECTIFICATION_APP_BASE_PATH
||
'/'
).
replace
(
/
\/
+$/
,
''
);
const
legacyFlowPaths
=
new
Set
([
LEGACY_FLOW_PATH
,
`
${
appBasePath
}${
LEGACY_FLOW_PATH
}
`
,
]);
if
(
typeof
window
!==
'undefined'
&&
legacyFlowPaths
.
has
(
window
.
location
.
pathname
))
{
window
.
location
.
replace
(
`
${
appBasePath
}
/problem_rectification_flow_table
${
window
.
location
.
search
}${
window
.
location
.
hash
}
`
,
);
}
export
async
function
getInitialState
()
{
return
{
name
:
'project_manager_xy'
,
...
...
src/components/record-workbench/index.jsx
View file @
3917767f
...
...
@@ -737,6 +737,7 @@ export default function RecordWorkbench({
</
Select
>
)
:
(
<
Input
type=
{
field
.
type
===
'month'
?
'month'
:
'text'
}
value=
{
filters
[
field
.
key
]
||
''
}
onChange=
{
(
event
)
=>
onFiltersChange
?.(
field
.
key
,
event
.
target
.
value
)
}
placeholder=
{
field
.
placeholder
}
...
...
src/pages/RecordDetailDrawer/hooks/useSubjectRecord.js
View file @
3917767f
...
...
@@ -102,7 +102,7 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName)
},
[
recordName
,
refreshKey
,
subjectName
]);
const
handleFieldUpdate
=
async
(
fieldPath
,
newValue
,
fieldDef
)
=>
{
if
(
readonly
||
!
editableRecord
)
{
if
(
readonly
||
remoteRecord
?.
metadata
?.
is_locked
===
true
||
!
editableRecord
)
{
return
;
}
...
...
@@ -122,7 +122,12 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName)
};
const
isFieldDisabled
=
(
fieldPath
,
currentView
)
=>
{
return
isFieldDisabledByView
(
fieldPath
,
currentView
);
return
(
fieldPath
===
'is_locked'
||
fieldPath
===
'metadata.is_locked'
||
remoteRecord
?.
metadata
?.
is_locked
===
true
||
isFieldDisabledByView
(
fieldPath
,
currentView
)
);
};
return
{
...
...
src/pages/RecordDetailDrawer/index.jsx
View file @
3917767f
...
...
@@ -124,6 +124,8 @@ const RecordDetailDrawer = (props, ref) => {
);
const
activeView
=
detailViews
.
find
((
view
)
=>
view
.
name
===
activeTabKey
)
||
detailViews
[
0
];
const
isRecordLocked
=
remoteRecord
?.
metadata
?.
is_locked
===
true
;
const
effectiveReadonly
=
readonly
||
isRecordLocked
;
const
drawerWidth
=
isFullscreen
?
'100vw'
:
typeof
width
===
'number'
?
`
${
width
}
px`
:
width
;
const
disabledFieldSet
=
useMemo
(
()
=>
...
...
@@ -214,22 +216,31 @@ const RecordDetailDrawer = (props, ref) => {
{
error
}
</
div
>
)
:
(
<
div
className=
"space-y-4"
>
{
isRecordLocked
?
(
<
div
className=
"rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800"
>
当前问题整改记录已锁定,仅可查看。如需修改,请先在问题清单中解锁。
</
div
>
)
:
null
}
<
SubjectViewContent
view=
{
activeView
}
subject=
{
subject
}
editableRecord=
{
editableRecord
}
onFieldUpdate=
{
handleFieldUpdate
}
onDataChange=
{
onDataChange
}
readonly=
{
r
eadonly
}
readonly=
{
effectiveR
eadonly
}
isFieldDisabled=
{
isDrawerFieldDisabled
}
/>
</
div
>
)
}
</
main
>
<
footer
className=
"border-t border-white/70 bg-white/82 px-6 py-4 backdrop-blur"
>
<
div
className=
"flex items-center justify-between gap-3"
>
<
div
className=
"text-sm text-zinc-500"
>
长文本、附件和状态类字段支持在详情面板内直接维护
{
isRecordLocked
?
'当前记录已锁定,所有业务字段均为只读'
:
'长文本、附件和状态类字段支持在详情面板内直接维护'
}
</
div
>
<
Button
outline
onClick=
{
closeDrawer
}
>
关闭
...
...
src/pages/problem_rectification/Layout.jsx
View file @
3917767f
...
...
@@ -27,7 +27,7 @@ const menuGroups = [
icon
:
HomeModernIcon
,
},
{
name
:
'
业务驾驶舱
'
,
name
:
'
整改分析
'
,
path
:
'/integrity_risk_table/business_dashboard'
,
icon
:
ChartPieIcon
,
},
...
...
src/pages/problem_rectification/dashboard/index.jsx
View file @
3917767f
...
...
@@ -711,10 +711,11 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
?
summary
.
focusIssueGroups
?.[
activeFocusIssueType
.
key
]
||
[]
:
[];
const
isBusinessDashboard
=
dashboardMode
===
'business'
;
const
dashboardTitle
=
isBusinessDashboard
?
'业务驾驶舱'
:
'问题整改驾驶舱'
;
const
dashboardEyebrow
=
isBusinessDashboard
?
'Rectification Analysis'
:
'Leadership Dashboard'
;
const
dashboardTitle
=
isBusinessDashboard
?
'问题整改分析'
:
'问题整改驾驶舱'
;
const
dashboardSubtitle
=
isBusinessDashboard
?
'围绕问题风险定位、部门责任追踪和整改质效评估,
沉淀
业务分析与督办依据。'
:
'
围绕整改完成进度、问题风险定位、部门责任压实和整改质效评估,统一展示今年问题
整改全景。'
;
?
'围绕问题风险定位、部门责任追踪和整改质效评估,
形成
业务分析与督办依据。'
:
'
面向管理层集中展示问题总量、整改进度、闭环情况和逾期风险,快速掌握
整改全景。'
;
const
yearLabel
=
activeYear
===
'all'
?
'全部年份'
:
`${activeYear} 年`
;
return
(
...
...
@@ -725,7 +726,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<
div
className=
"relative flex flex-wrap items-end justify-between gap-6"
>
<
div
className=
"max-w-4xl"
>
<
div
className=
"text-xs font-semibold uppercase tracking-[0.28em] text-white/75"
>
Integrity Risk Dashboard
{
dashboardEyebrow
}
</
div
>
<
h1
className=
"mt-3 text-3xl font-semibold tracking-tight md:text-[2.55rem]"
>
{
dashboardTitle
}
...
...
@@ -809,6 +810,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
{
!
loading
&&
!
dashboardData
.
isEmpty
&&
!
activeScope
.
isEmpty
?
(
<>
{
!
isBusinessDashboard
?
(
<>
<
section
className=
"space-y-5"
>
<
SectionHeading
eyebrow=
"Overview"
...
...
@@ -848,17 +850,13 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
/>
</
div
>
</
section
>
)
:
null
}
{
isBusinessDashboard
?
(
<>
<
section
className=
"space-y-5"
>
<
SectionHeading
eyebrow=
"Progress"
title=
"整改进度"
description=
"按整改进度统计分布与口径,
辅助判断当前积压状态和督办入口
。"
description=
"按整改进度统计分布与口径,
帮助管理层判断当前积压状态和重点督办方向
。"
/>
<
div
className=
"grid gap-6"
>
<
Panel
title=
"整改进度分布"
description=
"按整改进度统计数量与占比,直观看当前积压主要落在哪个状态。"
...
...
@@ -869,9 +867,12 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<
EmptyBlock
title=
"暂无整改进度分布数据"
/>
)
}
</
Panel
>
</
div
>
</
section
>
</>
)
:
null
}
{
isBusinessDashboard
?
(
<>
<
section
className=
"space-y-5"
>
<
SectionHeading
eyebrow=
"Risk"
...
...
@@ -997,11 +998,11 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<
SectionHeading
eyebrow=
"Quality"
title=
"审计整改质效"
description=
"
驾驶舱
只展示综合整改质效结果,具体评估维度在维护页补录和校验。"
description=
"
分析页
只展示综合整改质效结果,具体评估维度在维护页补录和校验。"
/>
<
Panel
title=
"综合整改质效结果"
description=
"按
高、中、低展示最终整改质效等级;暂按“两个及以上高为高、出现低则低、一个高则中”的初步规则计算,具体维度和规则后续确认
。"
description=
"按
六项整改评估字段自动计算并展示高、中、低分布;任一负向维度为低,至少两个正向维度为高,其余有效结果为中
。"
>
{
summary
.
qualityRatingDistribution
.
length
>
0
?
(
<
ReactECharts
option=
{
qualityRatingOption
}
style=
{
{
height
:
380
}
}
/>
...
...
src/pages/problem_rectification/flow-table/index.jsx
View file @
3917767f
...
...
@@ -25,7 +25,7 @@ import {
import
{
normalizeText
,
toArray
}
from
'@/utils/subjectView'
;
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
{
PencilSquareIcon
}
from
'@heroicons/react/24/outline'
;
import
{
LockClosedIcon
,
PencilSquareIcon
}
from
'@heroicons/react/24/outline'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useSearchParams
}
from
'umi'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
...
...
@@ -33,13 +33,17 @@ import {
AUDIT_STATUS_APPROVED
,
AUDIT_STATUS_PENDING
,
AUDIT_STATUS_REJECTED
,
DELAY_REQUEST_FIELD
,
EDITOR_STATUS_COMPLETED
,
EDITOR_STATUS_PENDING
,
FLOW_MODE_BATCH
,
FLOW_MODE_DELAY
,
FLOW_MODE_DELAY_DEPARTMENT
,
FLOW_MODE_DEPARTMENT
,
PROBLEM_RECTIFICATION_SUBJECT
,
buildDepartmentParentBatchUpdateData
,
getProblemRectificationFlowTableUrl
,
normalizeDelayRequest
,
normalizeDepartmentRectificationItems
,
}
from
'../problemRectificationListService'
;
...
...
@@ -49,6 +53,7 @@ const FLOW_MANAGED_PARENT_FIELDS = [
'audit_status'
,
'final_audit_status'
,
'department_rectification_items'
,
DELAY_REQUEST_FIELD
,
];
const
DEPARTMENT_MANAGED_PARENT_FIELDS
=
[
...
FLOW_MANAGED_PARENT_FIELDS
,
...
...
@@ -117,7 +122,7 @@ function collectUsers(ids = []) {
return
Array
.
from
(
new
Set
(
ids
.
map
((
item
)
=>
toId
(
item
)).
filter
(
Boolean
)));
}
async
function
createProgressTask
({
recipientId
,
title
,
currentUser
,
returnUrl
})
{
async
function
createProgressTask
({
recipientId
,
title
,
currentUser
,
returnUrl
,
fallbackOnError
=
true
})
{
if
(
!
recipientId
)
{
return
''
;
}
...
...
@@ -138,6 +143,9 @@ async function createProgressTask({ recipientId, title, currentUser, returnUrl }
return
result
?.
data
?.
taskId
||
buildProgressFallback
(
'progress'
);
}
catch
(
error
)
{
if
(
!
fallbackOnError
)
{
throw
error
;
}
return
buildProgressFallback
(
'progress'
);
}
}
...
...
@@ -200,6 +208,7 @@ function buildDepartmentRows(records = []) {
progressIdEditor
:
item
.
progress_id_editor
||
''
,
progressIdAuditor
:
item
.
progress_id_auditor
||
''
,
progressIdSender
:
item
.
progress_id_sender
||
''
,
isLocked
:
record
.
metadata
?.
is_locked
===
true
,
}));
});
}
...
...
@@ -234,10 +243,51 @@ function buildBatchRows(records = [], metadata = {}) {
progressIdEditor
:
toArray
(
metadata
.
progress_id_editor
)[
0
]
||
''
,
progressIdAuditor
:
toArray
(
metadata
.
progress_id_auditor
)[
0
]
||
''
,
progressIdSender
:
metadata
.
progress_id_sender
||
''
,
isLocked
:
recordMetadata
.
is_locked
===
true
,
};
});
}
function
buildDelayRows
(
records
=
[],
metadata
=
{})
{
const
editor
=
toId
(
toArray
(
metadata
.
editor
)[
0
]);
const
auditor
=
toId
(
toArray
(
metadata
.
auditor
)[
0
]);
return
records
.
flatMap
((
record
)
=>
{
const
recordMetadata
=
record
.
metadata
||
{};
const
request
=
normalizeDelayRequest
(
recordMetadata
[
DELAY_REQUEST_FIELD
]);
if
(
!
request
)
{
return
[];
}
return
[{
key
:
`
${
record
.
_id
||
record
.
name
}
-delay`
,
flowMode
:
FLOW_MODE_DELAY
,
record
,
item
:
null
,
itemId
:
record
.
_id
||
record
.
name
,
recordId
:
record
.
_id
||
record
.
name
,
title
:
record
.
title
||
recordMetadata
.
project_name
||
'-'
,
projectName
:
recordMetadata
.
project_name
||
'-'
,
problemBrief
:
recordMetadata
.
problem_brief
||
''
,
relatedIssueDomain
:
toArray
(
recordMetadata
.
related_issue_domain
).
join
(
'、'
),
department
:
toArray
(
recordMetadata
.
responsible_department
).
join
(
'、'
)
||
'-'
,
editor
:
request
.
editor
||
editor
,
auditor
:
request
.
auditor
||
auditor
,
sender
:
request
.
sender
||
toId
(
metadata
.
sender
),
originalDueDate
:
request
.
original_due_date
||
''
,
requestedDueDate
:
request
.
requested_due_date
||
''
,
delayReason
:
request
.
delay_reason
||
''
,
editorStatus
:
request
.
editor_status
||
EDITOR_STATUS_PENDING
,
auditStatus
:
request
.
auditor_status
||
AUDIT_STATUS_PENDING
,
finalAuditStatus
:
request
.
sender_status
||
AUDIT_STATUS_PENDING
,
progressIdEditor
:
request
.
progress_id_editor
||
''
,
progressIdAuditor
:
request
.
progress_id_auditor
||
''
,
progressIdSender
:
request
.
progress_id_sender
||
''
,
isLocked
:
recordMetadata
.
is_locked
===
true
,
}];
});
}
export
default
function
ProblemRectificationFlowTablePage
()
{
const
userInfo
=
useContext
(
UserContext
);
const
[
searchParams
]
=
useSearchParams
();
...
...
@@ -254,11 +304,16 @@ export default function ProblemRectificationFlowTablePage() {
const
metadata
=
useMemo
(()
=>
messageRecord
?.
metadata
||
{},
[
messageRecord
]);
const
flowMode
=
metadata
.
flow_mode
||
FLOW_MODE_BATCH
;
const
isDelayFlow
=
flowMode
===
FLOW_MODE_DELAY
||
flowMode
===
FLOW_MODE_DELAY_DEPARTMENT
;
const
rows
=
useMemo
(
()
=>
flowMode
===
FLOW_MODE_DEPARTMENT
()
=>
{
if
(
flowMode
===
FLOW_MODE_DELAY
||
flowMode
===
FLOW_MODE_DELAY_DEPARTMENT
)
{
return
buildDelayRows
(
records
,
metadata
);
}
return
flowMode
===
FLOW_MODE_DEPARTMENT
?
buildDepartmentRows
(
records
)
:
buildBatchRows
(
records
,
metadata
),
:
buildBatchRows
(
records
,
metadata
);
},
[
flowMode
,
metadata
,
records
],
);
const
currentUserId
=
toId
(
userInfo
?.
_id
);
...
...
@@ -356,6 +411,8 @@ export default function ProblemRectificationFlowTablePage() {
situation
:
row
.
situation
,
dueDate
:
formatDateInput
(
row
.
dueDate
),
proof
:
row
.
proof
,
requestedDueDate
:
formatDateInput
(
row
.
requestedDueDate
),
delayReason
:
row
.
delayReason
||
''
,
...(
drafts
[
row
.
key
]
||
{}),
});
...
...
@@ -390,6 +447,37 @@ export default function ProblemRectificationFlowTablePage() {
});
};
const
updateDelayRequest
=
async
(
row
,
requestPatch
,
recordPatch
=
{})
=>
{
const
latestRecord
=
await
loadRecord
(
row
.
recordId
);
if
(
latestRecord
?.
metadata
?.
is_locked
===
true
)
{
throw
new
Error
(
'当前问题整改记录已锁定,不允许继续修改或审批。'
);
}
const
currentRequest
=
normalizeDelayRequest
(
latestRecord
?.
metadata
?.[
DELAY_REQUEST_FIELD
],
);
if
(
!
currentRequest
)
{
throw
new
Error
(
'当前问题的延期申请不存在,请刷新后重试。'
);
}
const
currentFlowIds
=
[
messageRecord
?.
name
,
messageRecord
?.
_id
,
messageId
].
filter
(
Boolean
);
if
(
currentRequest
.
flow_record_id
&&
!
currentFlowIds
.
includes
(
currentRequest
.
flow_record_id
)
)
{
throw
new
Error
(
'当前延期申请已关联其他流程,请刷新后重试。'
);
}
await
updateRecord
(
row
.
recordId
,
{
metadata
:
{
...
recordPatch
,
[
DELAY_REQUEST_FIELD
]:
{
...
currentRequest
,
...
requestPatch
,
},
},
});
};
const
updateBatchRow
=
async
(
row
,
patch
)
=>
{
await
updateRecord
(
row
.
recordId
,
{
metadata
:
{
...
...
@@ -404,11 +492,20 @@ export default function ProblemRectificationFlowTablePage() {
};
const
withRowSubmit
=
async
(
row
,
handler
)
=>
{
if
(
row
.
isLocked
)
{
setError
(
'当前问题整改记录已锁定,不允许继续修改或审批。'
);
return
;
}
setSubmittingKey
(
row
.
key
);
setError
(
''
);
setSuccess
(
''
);
try
{
const
latestRecord
=
await
loadRecord
(
row
.
recordId
);
if
(
latestRecord
?.
metadata
?.
is_locked
===
true
)
{
throw
new
Error
(
'当前问题整改记录已锁定,不允许继续修改或审批。'
);
}
await
handler
();
await
loadFlow
();
setSuccess
(
'操作已完成。'
);
...
...
@@ -468,6 +565,139 @@ export default function ProblemRectificationFlowTablePage() {
});
};
const
saveDelayDraft
=
async
(
row
,
{
requireComplete
=
false
}
=
{})
=>
{
const
draft
=
getDraft
(
row
);
const
requestedDueDate
=
draft
.
requestedDueDate
?
dayjs
(
draft
.
requestedDueDate
).
toISOString
()
:
null
;
const
delayReason
=
normalizeText
(
draft
.
delayReason
);
if
(
requireComplete
&&
(
!
requestedDueDate
||
!
delayReason
))
{
throw
new
Error
(
'提交延期申请前,请填写新的预计完成时间和延期事由。'
);
}
const
patch
=
{
requested_due_date
:
requestedDueDate
,
delay_reason
:
delayReason
,
};
await
updateDelayRequest
(
row
,
patch
);
return
patch
;
};
const
handleDelaySave
=
async
(
row
)
=>
{
await
withRowSubmit
(
row
,
async
()
=>
{
await
saveDelayDraft
(
row
);
});
};
const
handleDelayEditorSubmit
=
async
(
row
)
=>
{
await
withRowSubmit
(
row
,
async
()
=>
{
const
savedPatch
=
await
saveDelayDraft
(
row
,
{
requireComplete
:
true
});
const
progressIdAuditor
=
await
createProgressTask
({
recipientId
:
row
.
auditor
,
title
:
`
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
延期申请一级审批 -
${
row
.
title
}
`
,
currentUser
:
userInfo
,
returnUrl
,
fallbackOnError
:
false
,
});
await
completeProgressTask
(
row
.
progressIdEditor
);
await
updateDelayRequest
(
row
,
{
...
savedPatch
,
editor_status
:
EDITOR_STATUS_COMPLETED
,
auditor_status
:
AUDIT_STATUS_PENDING
,
sender_status
:
AUDIT_STATUS_PENDING
,
progress_id_auditor
:
progressIdAuditor
,
progress_id_sender
:
''
,
submitted_at
:
dayjs
().
toISOString
(),
auditor_decided_at
:
null
,
sender_decided_at
:
null
,
});
await
sendNoticeToUsers
({
title
:
'延期申请待一级审批'
,
content
:
`请点击链接审批延期申请:
${
returnUrl
}
`
,
from
:
currentUserId
,
to
:
[
row
.
auditor
],
});
});
};
const
handleDelayAuditorDecision
=
async
(
row
,
approved
)
=>
{
await
withRowSubmit
(
row
,
async
()
=>
{
await
completeProgressTask
(
row
.
progressIdAuditor
);
const
recipientId
=
row
.
sender
||
senderId
;
const
progressIdSender
=
approved
?
await
createProgressTask
({
recipientId
,
title
:
`
${
dayjs
().
format
(
'YYYY-MM-DD'
)}
延期申请最终审批 -
${
row
.
title
}
`
,
currentUser
:
userInfo
,
returnUrl
,
fallbackOnError
:
false
,
})
:
''
;
const
progressIdEditor
=
approved
?
row
.
progressIdEditor
:
await
createProgressTask
({
recipientId
:
row
.
editor
,
title
:
`一级审批驳回,请重新填写延期申请 -
${
row
.
title
}
`
,
currentUser
:
userInfo
,
returnUrl
,
fallbackOnError
:
false
,
});
await
updateDelayRequest
(
row
,
{
auditor_status
:
approved
?
AUDIT_STATUS_APPROVED
:
AUDIT_STATUS_REJECTED
,
sender_status
:
AUDIT_STATUS_PENDING
,
editor_status
:
approved
?
EDITOR_STATUS_COMPLETED
:
EDITOR_STATUS_PENDING
,
progress_id_editor
:
progressIdEditor
,
progress_id_sender
:
progressIdSender
,
auditor_decided_at
:
dayjs
().
toISOString
(),
sender_decided_at
:
null
,
});
await
sendNoticeToUsers
({
title
:
approved
?
'延期申请待最终审批'
:
'延期申请一级审批驳回'
,
content
:
approved
?
`请点击链接最终审批延期申请:
${
returnUrl
}
`
:
`延期申请一级审批未通过,请点击链接修改后重新提交:
${
returnUrl
}
`
,
from
:
currentUserId
,
to
:
approved
?
[
recipientId
]
:
[
row
.
editor
],
});
});
};
const
handleDelaySenderDecision
=
async
(
row
,
approved
)
=>
{
await
withRowSubmit
(
row
,
async
()
=>
{
await
completeProgressTask
(
row
.
progressIdSender
);
const
progressIdEditor
=
approved
?
row
.
progressIdEditor
:
await
createProgressTask
({
recipientId
:
row
.
editor
,
title
:
`最终审批驳回,请重新填写延期申请 -
${
row
.
title
}
`
,
currentUser
:
userInfo
,
returnUrl
,
fallbackOnError
:
false
,
});
await
updateDelayRequest
(
row
,
{
sender_status
:
approved
?
AUDIT_STATUS_APPROVED
:
AUDIT_STATUS_REJECTED
,
editor_status
:
approved
?
EDITOR_STATUS_COMPLETED
:
EDITOR_STATUS_PENDING
,
progress_id_editor
:
progressIdEditor
,
sender_decided_at
:
dayjs
().
toISOString
(),
},
approved
?
{
the_time_of_rectification
:
row
.
requestedDueDate
}
:
{},
);
await
sendNoticeToUsers
({
title
:
approved
?
'延期申请已通过'
:
'延期申请最终审批驳回'
,
content
:
approved
?
`延期申请已审批通过,新的预计完成时间已生效:
${
returnUrl
}
`
:
`延期申请最终审批未通过,请点击链接修改后重新提交:
${
returnUrl
}
`
,
from
:
currentUserId
,
to
:
[
row
.
editor
],
});
});
};
const
handleEditorSubmit
=
async
(
row
)
=>
{
await
withRowSubmit
(
row
,
async
()
=>
{
const
savedPatch
=
await
saveRowDraft
(
row
);
...
...
@@ -619,20 +849,41 @@ export default function ProblemRectificationFlowTablePage() {
};
const
canEditRow
=
(
row
)
=>
!
row
.
isLocked
&&
(
isSender
||
(
row
.
editor
===
currentUserId
&&
row
.
editorStatus
!==
EDITOR_STATUS_COMPLETED
)
||
(
row
.
auditor
===
currentUserId
&&
row
.
auditStatus
===
AUDIT_STATUS_REJECTED
);
(
row
.
auditor
===
currentUserId
&&
row
.
auditStatus
===
AUDIT_STATUS_REJECTED
)
);
const
canSubmitRow
=
(
row
)
=>
!
row
.
isLocked
&&
row
.
editor
===
currentUserId
&&
row
.
editorStatus
!==
EDITOR_STATUS_COMPLETED
;
const
canAuditRow
=
(
row
)
=>
!
row
.
isLocked
&&
row
.
auditor
===
currentUserId
&&
row
.
editorStatus
===
EDITOR_STATUS_COMPLETED
&&
row
.
auditStatus
!==
AUDIT_STATUS_APPROVED
;
const
canFinalAuditRow
=
(
row
)
=>
!
row
.
isLocked
&&
senderId
===
currentUserId
&&
row
.
auditStatus
===
AUDIT_STATUS_APPROVED
&&
row
.
finalAuditStatus
!==
AUDIT_STATUS_APPROVED
;
const
canEditDelayRow
=
(
row
)
=>
!
row
.
isLocked
&&
row
.
editor
===
currentUserId
&&
row
.
editorStatus
!==
EDITOR_STATUS_COMPLETED
;
const
canSubmitDelayRow
=
canEditDelayRow
;
const
canAuditDelayRow
=
(
row
)
=>
!
row
.
isLocked
&&
row
.
auditor
===
currentUserId
&&
row
.
editorStatus
===
EDITOR_STATUS_COMPLETED
&&
row
.
auditStatus
!==
AUDIT_STATUS_APPROVED
;
const
canFinalAuditDelayRow
=
(
row
)
=>
!
row
.
isLocked
&&
(
row
.
sender
||
senderId
)
===
currentUserId
&&
row
.
editorStatus
===
EDITOR_STATUS_COMPLETED
&&
row
.
auditStatus
===
AUDIT_STATUS_APPROVED
&&
row
.
finalAuditStatus
!==
AUDIT_STATUS_APPROVED
;
return
(
<
div
className=
"space-y-6 p-6"
>
...
...
@@ -642,9 +893,12 @@ export default function ProblemRectificationFlowTablePage() {
<
div
className=
"text-xs font-semibold uppercase tracking-[0.24em] text-rose-600"
>
Problem Rectification Flow
</
div
>
<
h1
className=
"mt-2 text-2xl font-semibold tracking-tight text-slate-950"
>
问题整改表格处理
</
h1
>
<
h1
className=
"mt-2 text-2xl font-semibold tracking-tight text-slate-950"
>
{
isDelayFlow
?
'延期申请处理'
:
'问题整改表格处理'
}
</
h1
>
<
p
className=
"mt-2 text-sm text-slate-500"
>
当前流程:
{
messageId
||
'-'
}
;流程模式:
{
flowMode
===
FLOW_MODE_DEPARTMENT
?
'按责任部门'
:
'批量简单推送'
}
当前流程:
{
messageId
||
'-'
}
;流程模式:
{
isDelayFlow
?
'问题延期申请'
:
flowMode
===
FLOW_MODE_DEPARTMENT
?
'按责任部门'
:
'批量简单推送'
}
</
p
>
</
div
>
<
Button
outline
onClick=
{
loadFlow
}
disabled=
{
loading
||
Boolean
(
submittingKey
)
}
>
...
...
@@ -664,7 +918,9 @@ export default function ProblemRectificationFlowTablePage() {
{
loading
?
(
<
div
className=
"px-6 py-14 text-center text-sm text-slate-500"
>
正在加载流程表格...
</
div
>
)
:
rows
.
length
===
0
?
(
<
div
className=
"px-6 py-14 text-center text-sm text-slate-500"
>
当前流程没有可处理的整改明细。
</
div
>
<
div
className=
"px-6 py-14 text-center text-sm text-slate-500"
>
{
isDelayFlow
?
'当前流程没有可处理的延期申请。'
:
'当前流程没有可处理的整改明细。'
}
</
div
>
)
:
(
<
Table
bleed
dense
className=
"[--gutter:--spacing(4)]"
>
<
TableHead
>
...
...
@@ -674,10 +930,20 @@ export default function ProblemRectificationFlowTablePage() {
<
TableHeader
>
涉及业务领域
</
TableHeader
>
<
TableHeader
>
责任部门
</
TableHeader
>
<
TableHeader
>
填写人 / 审批人
</
TableHeader
>
{
isDelayFlow
?
(
<>
<
TableHeader
>
原预计完成时间
</
TableHeader
>
<
TableHeader
>
申请完成时间
</
TableHeader
>
<
TableHeader
>
延期事由
</
TableHeader
>
</>
)
:
(
<>
<
TableHeader
>
整改进度
</
TableHeader
>
<
TableHeader
>
预计完成
</
TableHeader
>
<
TableHeader
>
整改情况
</
TableHeader
>
<
TableHeader
>
证明材料
</
TableHeader
>
</>
)
}
<
TableHeader
>
状态
</
TableHeader
>
<
TableHeader
>
操作
</
TableHeader
>
</
TableRow
>
...
...
@@ -685,13 +951,21 @@ export default function ProblemRectificationFlowTablePage() {
<
TableBody
>
{
rows
.
map
((
row
)
=>
{
const
draft
=
getDraft
(
row
);
const
editable
=
canEditRow
(
row
);
const
editable
=
isDelayFlow
?
canEditDelayRow
(
row
)
:
canEditRow
(
row
);
const
submitting
=
submittingKey
===
row
.
key
;
return
(
<
TableRow
key=
{
row
.
key
}
className=
"align-top"
>
<
TableCell
className=
"min-w-64"
>
<
div
className=
"flex flex-wrap items-center gap-2"
>
<
div
className=
"font-medium text-slate-900"
>
{
row
.
title
}
</
div
>
{
row
.
isLocked
?
(
<
span
className=
"inline-flex items-center gap-1 rounded-full bg-amber-50 px-2.5 py-1 text-xs font-medium text-amber-700 ring-1 ring-amber-100"
>
<
LockClosedIcon
className=
"size-3.5"
aria
-
hidden=
"true"
/>
已锁定
</
span
>
)
:
null
}
</
div
>
<
div
className=
"mt-1 text-xs text-slate-500"
>
{
row
.
projectName
}
</
div
>
{
isSender
?
(
<
Button
...
...
@@ -701,7 +975,7 @@ export default function ProblemRectificationFlowTablePage() {
onClick=
{
()
=>
setEditingRecordName
(
row
.
record
?.
name
||
row
.
recordId
)
}
>
<
PencilSquareIcon
data
-
slot=
"icon"
/>
编辑问题记录
{
row
.
isLocked
?
'查看问题记录'
:
'编辑问题记录'
}
</
Button
>
)
:
null
}
</
TableCell
>
...
...
@@ -720,6 +994,42 @@ export default function ProblemRectificationFlowTablePage() {
<
div
className=
"text-xs text-slate-500"
>
填:
{
getUserLabel
(
row
.
editor
)
}
</
div
>
<
div
className=
"mt-1 text-xs text-slate-500"
>
审:
{
getUserLabel
(
row
.
auditor
)
}
</
div
>
</
TableCell
>
{
isDelayFlow
?
(
<>
<
TableCell
className=
"min-w-40"
>
{
formatDateDisplay
(
row
.
originalDueDate
)
}
</
TableCell
>
<
TableCell
className=
"min-w-44"
>
{
editable
?
(
<
Input
type=
"date"
value=
{
draft
.
requestedDueDate
}
aria
-
label=
{
`${row.department} 新的预计完成时间`
}
onChange=
{
(
event
)
=>
setDraftValue
(
row
.
key
,
{
requestedDueDate
:
event
.
target
.
value
})
}
/>
)
:
(
formatDateDisplay
(
row
.
requestedDueDate
)
)
}
</
TableCell
>
<
TableCell
className=
"min-w-72"
>
{
editable
?
(
<
Textarea
rows=
{
4
}
value=
{
draft
.
delayReason
}
aria
-
label=
{
`${row.department} 延期事由`
}
onChange=
{
(
event
)
=>
setDraftValue
(
row
.
key
,
{
delayReason
:
event
.
target
.
value
})
}
/>
)
:
(
<
span
className=
"whitespace-pre-wrap"
>
{
row
.
delayReason
||
'-'
}
</
span
>
)
}
</
TableCell
>
</>
)
:
(
<>
<
TableCell
className=
"min-w-36"
>
{
editable
?
(
<
Select
...
...
@@ -776,15 +1086,64 @@ export default function ProblemRectificationFlowTablePage() {
/>
)
:
null
}
</
TableCell
>
</>
)
}
<
TableCell
className=
"min-w-44"
>
<
div
className=
"space-y-1.5"
>
<
div
className=
"flex items-center justify-between gap-2"
>
<
span
className=
"text-xs text-slate-500"
>
填写
</
span
>
<
StatusPill
value=
{
row
.
editorStatus
}
/>
</
div
>
<
div
className=
"flex items-center justify-between gap-2"
>
<
span
className=
"text-xs text-slate-500"
>
一级审批
</
span
>
<
StatusPill
value=
{
row
.
auditStatus
}
/>
</
div
>
<
div
className=
"flex items-center justify-between gap-2"
>
<
span
className=
"text-xs text-slate-500"
>
最终审批
</
span
>
<
StatusPill
value=
{
row
.
finalAuditStatus
}
/>
</
div
>
</
div
>
</
TableCell
>
<
TableCell
className=
"min-w-52"
>
<
div
className=
"flex flex-wrap gap-2"
>
{
isDelayFlow
?
(
<>
{
editable
?
(
<
Button
outline
disabled=
{
submitting
}
onClick=
{
()
=>
handleDelaySave
(
row
)
}
>
保存申请
</
Button
>
)
:
null
}
{
canSubmitDelayRow
(
row
)
?
(
<
Button
color=
"blue"
disabled=
{
submitting
}
onClick=
{
()
=>
handleDelayEditorSubmit
(
row
)
}
>
提交申请
</
Button
>
)
:
null
}
{
canAuditDelayRow
(
row
)
?
(
<>
<
Button
color=
"emerald"
disabled=
{
submitting
}
onClick=
{
()
=>
handleDelayAuditorDecision
(
row
,
true
)
}
>
一级通过
</
Button
>
<
Button
color=
"red"
disabled=
{
submitting
}
onClick=
{
()
=>
handleDelayAuditorDecision
(
row
,
false
)
}
>
一级驳回
</
Button
>
</>
)
:
null
}
{
canFinalAuditDelayRow
(
row
)
?
(
<>
<
Button
color=
"blue"
disabled=
{
submitting
}
onClick=
{
()
=>
handleDelaySenderDecision
(
row
,
true
)
}
>
最终通过
</
Button
>
<
Button
color=
"red"
disabled=
{
submitting
}
onClick=
{
()
=>
handleDelaySenderDecision
(
row
,
false
)
}
>
最终驳回
</
Button
>
</>
)
:
null
}
{
!
editable
&&
!
canAuditDelayRow
(
row
)
&&
!
canFinalAuditDelayRow
(
row
)
?
(
<
span
className=
"text-xs text-slate-400"
>
当前无可执行操作
</
span
>
)
:
null
}
</>
)
:
(
<>
{
editable
?
(
<
Button
outline
disabled=
{
submitting
}
onClick=
{
()
=>
handleSave
(
row
)
}
>
保存
...
...
@@ -818,6 +1177,8 @@ export default function ProblemRectificationFlowTablePage() {
{
!
editable
&&
!
canAuditRow
(
row
)
&&
!
canFinalAuditRow
(
row
)
?
(
<
span
className=
"text-xs text-slate-400"
>
当前无可执行操作
</
span
>
)
:
null
}
</>
)
}
</
div
>
</
TableCell
>
</
TableRow
>
...
...
@@ -834,7 +1195,7 @@ export default function ProblemRectificationFlowTablePage() {
subjectName=
{
PROBLEM_RECTIFICATION_SUBJECT
}
readonly=
{
!
isSender
}
disabledFields=
{
flowMode
===
FLOW_MODE_DEPARTMENT
flowMode
===
FLOW_MODE_DEPARTMENT
||
isDelayFlow
?
DEPARTMENT_MANAGED_PARENT_FIELDS
:
FLOW_MANAGED_PARENT_FIELDS
}
...
...
src/pages/problem_rectification/form/index.jsx
View file @
3917767f
...
...
@@ -15,10 +15,11 @@ import {
TableRow
,
}
from
'@/catalyst-ui-kit/javascript/table'
;
import
TailwindEditableField
from
'@/pages/RecordDetailDrawer/components/TailwindEditableField'
;
import
{
updateRecord
}
from
'@/services/DataService'
;
import
{
loadRecord
,
updateRecord
}
from
'@/services/DataService'
;
import
{
normalizeText
}
from
'@/utils/subjectView'
;
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
{
LockClosedIcon
}
from
'@heroicons/react/24/outline'
;
import
{
useContext
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useSearchParams
}
from
'umi'
;
import
{
UserContext
}
from
'@/wrapper/Auth'
;
...
...
@@ -180,6 +181,7 @@ export default function ProblemRectificationFormPage() {
const
currentStep
=
flowData
?.
currentStep
||
'editor'
;
const
viewerRole
=
flowData
?.
viewerRole
||
'guest'
;
const
canEditRecords
=
Boolean
(
flowData
?.
canEditRecords
);
const
hasLockedRecords
=
relatedRecords
.
some
((
record
)
=>
record
.
isLocked
);
const
metadata
=
messageRecord
?.
metadata
||
{};
const
fieldMap
=
useMemo
(()
=>
{
...
...
@@ -227,6 +229,13 @@ export default function ProblemRectificationFormPage() {
return
;
}
const
latestRecord
=
await
loadRecord
(
selectedRecord
.
id
);
if
(
selectedRecord
.
isLocked
||
latestRecord
?.
metadata
?.
is_locked
===
true
)
{
setError
(
'当前问题整改记录已锁定,不允许修改。'
);
await
refreshAfterMutation
();
return
;
}
await
updateRecord
(
selectedRecord
.
id
,
{
metadata
:
{
[
fieldName
]:
...
...
@@ -246,6 +255,11 @@ export default function ProblemRectificationFormPage() {
return
;
}
if
(
hasLockedRecords
)
{
setError
(
'当前流程包含已锁定的问题整改记录,不允许继续修改或审批。'
);
return
;
}
setSubmitting
(
true
);
setError
(
''
);
...
...
@@ -307,8 +321,9 @@ export default function ProblemRectificationFormPage() {
});
};
const
actionHint
=
viewerRole
===
'editor'
const
actionHint
=
hasLockedRecords
?
'当前流程包含已锁定记录,仅可查看;解锁后方可继续处理。'
:
viewerRole
===
'editor'
?
'你当前是填写人,请完善整改内容后点击送审。'
:
viewerRole
===
'auditor'
?
'你当前是复核人,请核对整改内容后进行通过或驳回。'
...
...
@@ -418,7 +433,15 @@ export default function ProblemRectificationFormPage() {
{
relatedRecords
.
map
((
record
)
=>
(
<
TableRow
key=
{
record
.
id
}
>
<
TableCell
>
<
div
className=
"flex flex-wrap items-center gap-2"
>
<
div
className=
"font-medium text-slate-900"
>
{
record
.
title
||
'-'
}
</
div
>
{
record
.
isLocked
?
(
<
span
className=
"inline-flex items-center gap-1 rounded-full bg-amber-50 px-2.5 py-1 text-xs font-medium text-amber-700 ring-1 ring-amber-100"
>
<
LockClosedIcon
className=
"size-3.5"
aria
-
hidden=
"true"
/>
已锁定
</
span
>
)
:
null
}
</
div
>
<
div
className=
"mt-1 text-xs text-slate-500"
>
{
record
.
projectName
||
'-'
}
</
div
>
</
TableCell
>
<
TableCell
>
{
record
.
feedbackUnit
||
'-'
}
</
TableCell
>
...
...
@@ -437,7 +460,7 @@ export default function ProblemRectificationFormPage() {
outline
onClick=
{
()
=>
setSelectedRecord
(
record
)
}
>
{
canEditRecords
?
'查看/填写'
:
'查看详情'
}
{
canEditRecords
&&
!
record
.
isLocked
?
'查看/填写'
:
'查看详情'
}
</
Button
>
</
TableCell
>
</
TableRow
>
...
...
@@ -465,17 +488,17 @@ export default function ProblemRectificationFormPage() {
</
div
>
<
div
className=
"mt-5 flex flex-wrap gap-3"
>
{
currentStep
===
'editor'
&&
viewerRole
===
'editor'
?
(
<
Button
color=
"blue"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleEditorSubmit
}
>
<
Button
color=
"blue"
disabled=
{
submitting
||
hasLockedRecords
||
relatedRecords
.
length
===
0
}
onClick=
{
handleEditorSubmit
}
>
{
submitting
?
'送审中...'
:
'填写完成并送审'
}
</
Button
>
)
:
null
}
{
currentStep
===
'auditor'
&&
viewerRole
===
'auditor'
?
(
<>
<
Button
color=
"emerald"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleAuditorApprove
}
>
<
Button
color=
"emerald"
disabled=
{
submitting
||
hasLockedRecords
||
relatedRecords
.
length
===
0
}
onClick=
{
handleAuditorApprove
}
>
{
submitting
?
'处理中...'
:
'复核通过'
}
</
Button
>
<
Button
color=
"red"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleAuditorReject
}
>
<
Button
color=
"red"
disabled=
{
submitting
||
hasLockedRecords
||
relatedRecords
.
length
===
0
}
onClick=
{
handleAuditorReject
}
>
{
submitting
?
'处理中...'
:
'驳回复填'
}
</
Button
>
</>
...
...
@@ -483,10 +506,10 @@ export default function ProblemRectificationFormPage() {
{
currentStep
===
'sender'
&&
viewerRole
===
'sender'
?
(
<>
<
Button
color=
"blue"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleSenderApprove
}
>
<
Button
color=
"blue"
disabled=
{
submitting
||
hasLockedRecords
||
relatedRecords
.
length
===
0
}
onClick=
{
handleSenderApprove
}
>
{
submitting
?
'处理中...'
:
'最终审核通过'
}
</
Button
>
<
Button
color=
"red"
disabled=
{
submitting
||
relatedRecords
.
length
===
0
}
onClick=
{
handleSenderReject
}
>
<
Button
color=
"red"
disabled=
{
submitting
||
hasLockedRecords
||
relatedRecords
.
length
===
0
}
onClick=
{
handleSenderReject
}
>
{
submitting
?
'处理中...'
:
'最终审核驳回'
}
</
Button
>
</>
...
...
@@ -512,6 +535,11 @@ export default function ProblemRectificationFormPage() {
<
DialogBody
>
{
selectedRecord
?
(
<
div
className=
"space-y-4"
>
{
selectedRecord
.
isLocked
?
(
<
div
className=
"rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800"
>
当前问题整改记录已锁定,仅可查看。
</
div
>
)
:
null
}
<
div
className=
"grid gap-4 md:grid-cols-2"
>
<
SummaryCard
label=
"问题标题"
value=
{
selectedRecord
.
title
||
'-'
}
/>
<
SummaryCard
label=
"反馈单位"
value=
{
selectedRecord
.
feedbackUnit
||
'-'
}
/>
...
...
@@ -526,8 +554,8 @@ export default function ProblemRectificationFormPage() {
label=
{
fieldDef
.
label
||
fieldDef
.
name
}
fieldDef=
{
fieldDef
}
value=
{
selectedRecord
.
raw
?.
metadata
?.[
fieldDef
.
name
]
}
readonly=
{
!
canEditRecords
}
disabled=
{
!
canEditRecords
}
readonly=
{
!
canEditRecords
||
selectedRecord
.
isLocked
}
disabled=
{
!
canEditRecords
||
selectedRecord
.
isLocked
}
onSave=
{
(
nextValue
)
=>
handleSaveField
(
fieldDef
.
name
,
nextValue
)
}
/>
))
}
...
...
src/pages/problem_rectification/list/index.jsx
View file @
3917767f
...
...
@@ -34,6 +34,7 @@ import {
}
from
'@/services/DataService'
;
import
*
as
Headless
from
'@headlessui/react'
;
import
{
ChevronRightIcon
}
from
'@heroicons/react/20/solid'
;
import
{
LockClosedIcon
,
LockOpenIcon
}
from
'@heroicons/react/24/outline'
;
import
clsx
from
'clsx'
;
import
dayjs
from
'dayjs'
;
import
{
useCallback
,
useEffect
,
useMemo
,
useState
}
from
'react'
;
...
...
@@ -48,9 +49,11 @@ import {
DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE
,
PROBLEM_RECTIFICATION_SUBJECT
,
AUDIT_STATUS_PENDING
,
DELAY_REQUEST_FIELD
,
DEPARTMENT_RECTIFICATION_FIELD
,
EDITOR_STATUS_PENDING
,
FLOW_MODE_BATCH
,
FLOW_MODE_DELAY
,
FLOW_MODE_DEPARTMENT
,
PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS
,
buildDepartmentAssignmentsFromRecord
,
...
...
@@ -66,6 +69,7 @@ import {
loadProblemRectificationListData
,
loadProblemRectificationListResources
,
loadProblemRectificationViewCountMap
,
normalizeDelayRequest
,
normalizeDepartmentRectificationItems
,
}
from
'../problemRectificationListService'
;
...
...
@@ -85,7 +89,6 @@ const FIELD_HINTS = {
problem_severity_level
:
'建议统一填写“高 / 中 / 低”'
,
rectification_timeliness_level
:
'建议统一填写“高 / 中 / 低”'
,
rectification_material_quality_level
:
'建议统一填写“高 / 中 / 低”'
,
rectification_overall_rating
:
'建议统一填写“高 / 中 / 低”'
,
involves_policy_revision
:
'建议统一填写“是”或“否”'
,
involves_system_construction
:
'建议统一填写“是”或“否”'
,
is_historical_problem
:
'建议统一填写“是”或“否”'
,
...
...
@@ -113,7 +116,6 @@ const CREATE_FIELD_OPTION_OVERRIDES = {
involves_system_construction
:
[
'是'
,
'否'
],
is_historical_problem
:
[
'是'
,
'否'
],
is_repeated_problem
:
[
'是'
,
'否'
],
rectification_overall_rating
:
[
'高'
,
'中'
,
'低'
],
};
function
StatusPill
({
text
,
tone
=
'default'
})
{
...
...
@@ -491,7 +493,7 @@ function FieldValuePreview({ column, record }) {
}
if (fieldName === 'rectification_overall_rating') {
const text =
formatFieldValue(value, column.fieldDef) || '未填写
';
const text =
record.overallRating || '待计算
';
return <StatusPill text={text} tone={resolveLevelTone(text, true)} />;
}
...
...
@@ -576,6 +578,7 @@ export default function ProblemRectificationListPage() {
const [actionLoading, setActionLoading] = useState(false);
const [filters, setFilters] = useState({
keyword: '',
inspectionReportMonth: '',
progress: '',
auditStatus: '',
severityLevel: '',
...
...
@@ -589,11 +592,14 @@ export default function ProblemRectificationListPage() {
const [selectedIds, setSelectedIds] = useState([]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showPushModal, setShowPushModal] = useState(false);
const [showDelayPushModal, setShowDelayPushModal] = useState(false);
const [cloneTargetRecord, setCloneTargetRecord] = useState(null);
const [formValues, setFormValues] = useState({});
const [fillerUsers, setFillerUsers] = useState([]);
const [auditorUsers, setAuditorUsers] = useState([]);
const [departmentAssignments, setDepartmentAssignments] = useState([]);
const [delayEditorUsers, setDelayEditorUsers] = useState([]);
const [delayAuditorUsers, setDelayAuditorUsers] = useState([]);
const [successMessage, setSuccessMessage] = useState('');
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE);
...
...
@@ -785,6 +791,7 @@ export default function ProblemRectificationListPage() {
() => filteredRecords.filter((item) => selectedIds.includes(item.id)),
[filteredRecords, selectedIds],
);
const hasLockedSelection = selectedRecords.some((record) => record.isLocked);
useEffect(() => {
const visibleIds = new Set(filteredRecords.map((item) => item.id));
...
...
@@ -847,6 +854,11 @@ export default function ProblemRectificationListPage() {
return;
}
if (targetRecords.some((record) => record.isLocked)) {
setError('已锁定的问题整改记录不允许删除,请先解锁。');
return;
}
const confirmed = window.confirm(`
确认删除
$
{
targetRecords
.
length
}
条问题整改记录吗?此操作不可撤销。
`);
if (!confirmed) {
return;
...
...
@@ -882,6 +894,12 @@ export default function ProblemRectificationListPage() {
return;
}
if (cloneTargetRecord.isLocked) {
setError('已锁定的问题整改记录不允许复制,请先解锁。');
setCloneTargetRecord(null);
return;
}
const recordName = cloneTargetRecord.name || cloneTargetRecord.id;
if (!recordName) {
setError('当前记录缺少可复制的 name 标识。');
...
...
@@ -906,6 +924,41 @@ export default function ProblemRectificationListPage() {
}
};
const handleLockToggle = async (record, nextLocked) => {
if (!record) {
return;
}
if (nextLocked && record.progress !== '已整改') {
setError('只有整改进度为“已整改”的记录才能锁定。');
return;
}
const recordName = record.name || record.id;
if (!recordName) {
setError('当前记录缺少可更新的标识。');
return;
}
setActionLoading(true);
setError('');
setSuccessMessage('');
try {
await updateRecord(recordName, {
metadata: {
is_locked: nextLocked,
},
});
await reloadList({ keepSuccessMessage: true });
setSuccessMessage(nextLocked ? '问题整改记录已锁定。' : '问题整改记录已解锁。');
} catch (requestError) {
setError(requestError?.message || `
$
{
nextLocked
?
'锁定'
:
'解锁'
}
问题整改记录失败,请稍后重试。
`);
} finally {
setActionLoading(false);
}
};
const handleSearchUsers = useCallback(async (keyword) => {
const filter = keyword.trim()
? {
...
...
@@ -933,6 +986,159 @@ export default function ProblemRectificationListPage() {
);
};
const closeDelayPushModal = () => {
setShowDelayPushModal(false);
setDelayEditorUsers([]);
setDelayAuditorUsers([]);
};
const openDelayPushModal = () => {
if (selectedRecords.length !== 1) {
setError('延期申请每次只能选择 1 条问题整改记录。');
return;
}
const [record] = selectedRecords;
if (record.isLocked) {
setError('已锁定的问题整改记录不允许发起延期申请,请先解锁。');
return;
}
if (record.delayRequest) {
setError('当前问题已发起延期申请,不能重复推送。');
return;
}
setError('');
setDelayEditorUsers([]);
setDelayAuditorUsers([]);
setShowDelayPushModal(true);
};
const handleDelayPush = async () => {
const editorProfile = delayEditorUsers[0];
const auditorProfile = delayAuditorUsers[0];
if (selectedRecords.length !== 1) {
setError('延期申请每次只能选择 1 条问题整改记录。');
return;
}
if (!editorProfile?._id || !auditorProfile?._id) {
setError('请分别选择 1 个填写人和 1 个一级审批人。');
return;
}
setActionLoading(true);
setError('');
setSuccessMessage('');
try {
const [selectedRecord] = selectedRecords;
const currentUserInfo = JSON.parse(window.localStorage.getItem('current_user') || '{}') || {};
const currentUserId = currentUserInfo?._id || currentUserInfo?.id || '';
if (!currentUserId) {
throw new Error('无法获取当前登录用户,不能发起延期申请。');
}
const latestRecord = await loadRecord(selectedRecord.id);
if (latestRecord?.metadata?.is_locked === true) {
throw new Error('当前问题整改记录已锁定,不允许发起延期申请。');
}
if (normalizeDelayRequest(latestRecord?.metadata?.[DELAY_REQUEST_FIELD])) {
throw new Error('当前问题已发起延期申请,不能重复推送。');
}
const editorId = editorProfile._id;
const auditorId = auditorProfile._id;
const messageRecord = await createRecord({
subject: 'message_reference_id',
title: `
推送延期申请填写
$
{
dayjs
().
format
(
'YYYY-MM-DD HH:mm'
)}
`,
metadata: {
sender: currentUserId,
editor: [editorId],
auditor: [auditorId],
restriction_key: '_id',
restriction_value: [selectedRecord.id],
reference_record_id: [selectedRecord.id],
view_name: activeGridView?.name || '',
subject_name: PROBLEM_RECTIFICATION_SUBJECT,
flow_mode: FLOW_MODE_DELAY,
department_item_ids: [],
editor_status: EDITOR_STATUS_PENDING,
auditor_status: EDITOR_STATUS_PENDING,
sender_status: EDITOR_STATUS_PENDING,
},
});
const flowRecordId = messageRecord.name || messageRecord._id;
const returnUrl = getProblemRectificationFlowTableUrl(flowRecordId);
const oaProcessResult = await executeAction('oa_notice:createProgress', {
title: `
$
{
dayjs
().
format
(
'YYYY-MM-DD'
)}
填写延期申请
-
$
{
selectedRecord
.
title
||
selectedRecord
.
projectName
}
`,
classify: '流程待办',
contentType: '流程待办',
receiveUserId: editorId,
receiveUserName: editorProfile.username || editorId,
receiveDisplayName: editorProfile.display_name || editorProfile.username || editorId,
sendUserId: currentUserId,
sendUserName: currentUserInfo?.username || '',
sendDisplayName: currentUserInfo?.display_name || '',
returnUrl,
});
const progressIdEditor = oaProcessResult?.data?.taskId || '';
const delayRequest = {
original_due_date: latestRecord?.metadata?.the_time_of_rectification || null,
requested_due_date: null,
delay_reason: '',
editor: editorId,
auditor: auditorId,
sender: currentUserId,
editor_status: EDITOR_STATUS_PENDING,
auditor_status: AUDIT_STATUS_PENDING,
sender_status: AUDIT_STATUS_PENDING,
progress_id_editor: progressIdEditor,
progress_id_auditor: '',
progress_id_sender: '',
flow_record_id: flowRecordId,
submitted_at: null,
auditor_decided_at: null,
sender_decided_at: null,
};
await updateRecord(selectedRecord.id, {
metadata: { [DELAY_REQUEST_FIELD]: delayRequest },
});
await updateRecord(messageRecord._id || messageRecord.name, {
metadata: { progress_id_editor: [progressIdEditor] },
});
const savedRecord = await loadRecord(selectedRecord.id);
const savedDelayRequest = normalizeDelayRequest(
savedRecord?.metadata?.[DELAY_REQUEST_FIELD],
);
if (savedDelayRequest?.flow_record_id !== flowRecordId) {
throw new Error('延期申请写入失败,请刷新后重试。');
}
const noticeResult = await createNotice({
title: '推送延期申请填写',
content: `
请点击链接填写延期申请:
$
{
returnUrl
}
`,
type: 'internal',
from: currentUserId,
to: [editorId],
settings: {},
});
await sendNotice(noticeResult.name);
closeDelayPushModal();
setSelectedIds([]);
await reloadList({ keepSuccessMessage: true });
setSuccessMessage('延期申请已推送,当前问题已进入填写流程。');
} catch (requestError) {
setError(requestError?.message || '推送延期申请失败,请稍后重试。');
} finally {
setActionLoading(false);
}
};
const handlePush = async () => {
const fillerIds = fillerUsers.map((user) => user._id).filter(Boolean);
const auditorIds = auditorUsers.map((user) => user._id).filter(Boolean);
...
...
@@ -943,6 +1149,11 @@ export default function ProblemRectificationListPage() {
return;
}
if (selectedRecords.some((record) => record.isLocked)) {
setError('已锁定的问题整改记录不允许推送,请先解锁。');
return;
}
if (isDepartmentFlow) {
const hasMissingDepartmentUser = departmentAssignments.some(
(item) => !item.editor?._id || !item.auditor?._id,
...
...
@@ -1122,6 +1333,11 @@ export default function ProblemRectificationListPage() {
type: 'text',
placeholder: '搜索项目名称、严重程度、分类、责任部门、问题原因...',
},
{
key: 'inspectionReportMonth',
label: '检查报告出具月份',
type: 'month',
},
{
key: 'progress',
label: '整改进度',
...
...
@@ -1191,6 +1407,7 @@ export default function ProblemRectificationListPage() {
setPage(0);
setFilters({
keyword: '',
inspectionReportMonth: '',
progress: '',
auditStatus: '',
severityLevel: '',
...
...
@@ -1285,9 +1502,16 @@ export default function ProblemRectificationListPage() {
key: 'push',
label: '推送整改表',
variant: 'outline',
disabled: () => selectedIds.length === 0 || actionLoading,
disabled: () => selectedIds.length === 0 ||
hasLockedSelection ||
actionLoading,
onClick: () => setShowPushModal(true),
},
{
key: 'push-delay-request',
label: '推送延期申请',
variant: 'outline',
disabled: () => selectedIds.length !== 1 || hasLockedSelection || actionLoading,
onClick: openDelayPushModal,
},
{
key: 'export',
label: '导出',
...
...
@@ -1307,7 +1531,7 @@ export default function ProblemRectificationListPage() {
label: '删除选中',
variant: 'solid',
tone: 'danger',
disabled: () => selectedIds.length === 0 || actionLoading,
disabled: () => selectedIds.length === 0 ||
hasLockedSelection ||
actionLoading,
onClick: () => handleDelete(selectedRecords),
},
]}
...
...
@@ -1318,11 +1542,29 @@ export default function ProblemRectificationListPage() {
variant: 'plain',
onClick: ({ openDetail, record }) => openDetail(record),
},
{
key: 'lock',
label: '锁定',
icon: LockClosedIcon,
variant: 'outline',
hidden: ({ record }) => record.isLocked || record.progress !== '已整改',
disabled: () => actionLoading,
onClick: ({ record }) => handleLockToggle(record, true),
},
{
key: 'unlock',
label: '解锁',
icon: LockOpenIcon,
variant: 'outline',
hidden: ({ record }) => !record.isLocked,
disabled: () => actionLoading,
onClick: ({ record }) => handleLockToggle(record, false),
},
{
key: 'clone',
label: '复制',
variant: 'outline',
disabled: (
) => actionLoading
,
disabled: (
{ record }) => actionLoading || record.isLocked
,
onClick: ({ record }) => setCloneTargetRecord(record),
},
{
...
...
@@ -1330,6 +1572,7 @@ export default function ProblemRectificationListPage() {
label: '删除',
variant: 'outline',
tone: 'danger',
disabled: ({ record }) => actionLoading || record.isLocked,
onClick: ({ record }) => handleDelete([record]),
},
]}
...
...
@@ -1342,6 +1585,7 @@ export default function ProblemRectificationListPage() {
recordName={recordName}
onClose={onClose}
onDataChange={onDataChange}
disabledFields={[DELAY_REQUEST_FIELD]}
preferredViewName={activeGridView?.title || activeGridView?.name}
/>
),
...
...
@@ -1353,7 +1597,7 @@ export default function ProblemRectificationListPage() {
loadingTitle: '正在加载问题整改列表',
loadingDescription: '正在读取对象结构、视图配置和记录数据,请稍候。',
emptyTitle: '当前视图暂无数据',
emptyDescription: '可以切换其他视图,或调整严重程度、整改评级、主责部门和重点关注筛选。',
emptyDescription: '可以切换其他视图,或调整
检查报告出具月份、
严重程度、整改评级、主责部门和重点关注筛选。',
}}
pagination={{
page,
...
...
@@ -1376,13 +1620,17 @@ export default function ProblemRectificationListPage() {
const fieldPreview = <FieldValuePreview record={record} column={column} />;
const fieldName = getFieldNameFromPath(column.field);
if (fieldName !== 'title'
|| !expandable
) {
if (fieldName !== 'title') {
return fieldPreview;
}
return (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
{fieldPreview}
{record.isLocked ? (
<StatusPill text="已锁定" tone="amber" />
) : null}
{expandable ? (
<Button
plain
className="text-rose-700"
...
...
@@ -1395,6 +1643,7 @@ export default function ProblemRectificationListPage() {
/>
{expanded ? '收起明细' : `
$
{
record
.
departmentItems
.
length
}
条部门明细
`}
</Button>
) : null}
</div>
);
}}
...
...
@@ -1574,6 +1823,61 @@ export default function ProblemRectificationListPage() {
</Dialog>
) : null}
{showDelayPushModal ? (
<Dialog open={showDelayPushModal} onClose={closeDelayPushModal} size="2xl">
<DialogTitle>推送延期申请</DialogTitle>
<DialogDescription>
填写人将在流程页补充申请完成时间和延期事由,最终审批通过前不会修改当前预计完成时间。
</DialogDescription>
<DialogBody>
<div className="rounded-lg border border-zinc-200 bg-zinc-50/80 p-4">
<div className="text-sm font-medium text-slate-900">
{selectedRecords[0]?.title || selectedRecords[0]?.projectName || '当前问题'}
</div>
<div className="mt-2 text-sm text-slate-600">
当前预计完成时间:
{formatFieldValue(
selectedRecords[0]?.raw?.metadata?.the_time_of_rectification,
{ type: 'date' },
) || '未设置'}
</div>
</div>
<div className="mt-6 grid gap-6 md:grid-cols-2">
<RemoteUserSelect
label="填写人"
value={delayEditorUsers}
onChange={(users) => setDelayEditorUsers(users.slice(-1))}
onSearch={handleSearchUsers}
placeholder="搜索延期申请填写人"
/>
<RemoteUserSelect
label="一级审批人"
value={delayAuditorUsers}
onChange={(users) => setDelayAuditorUsers(users.slice(-1))}
onSearch={handleSearchUsers}
placeholder="搜索延期申请一级审批人"
/>
</div>
</DialogBody>
<DialogActions>
<Button outline onClick={closeDelayPushModal} disabled={actionLoading}>
取消
</Button>
<Button
color="blue"
disabled={
actionLoading ||
delayEditorUsers.length !== 1 ||
delayAuditorUsers.length !== 1
}
onClick={handleDelayPush}
>
{actionLoading ? '推送中...' : '确认推送'}
</Button>
</DialogActions>
</Dialog>
) : null}
{cloneTargetRecord ? (
<Dialog open={Boolean(cloneTargetRecord)} onClose={() => setCloneTargetRecord(null)} size="lg">
<DialogTitle>复制问题整改记录</DialogTitle>
...
...
src/pages/problem_rectification/problem-rectification.business-flow.md
View file @
3917767f
...
...
@@ -41,14 +41,14 @@
建议在当前项目中保留以下四个一级入口:
-
驾驶舱:
`/integrity_risk_table/dashboard`
-
业务驾驶舱
:
`/integrity_risk_table/business_dashboard`
-
整改分析
:
`/integrity_risk_table/business_dashboard`
-
问题整改:
`/problem_rectification`
-
往期材料归档:
`/issue_rectification_archive`
建议交互形态:
-
驾驶舱
仅展示顶部横幅、部门分组切换与宏观总览 KPI 卡片
-
业务驾驶舱承接整改进度分布、问题风险定位、责任追踪与整改质效图表
-
驾驶舱
面向管理层,展示顶部横幅、部门分组切换、宏观总览 KPI 与整改进度分布
-
整改分析承接问题风险定位、责任追踪与整改质效图表;“驾驶舱”命名仅用于管理层入口
-
问题整改主列表使用普通数据表格,不再依赖多维表格编辑器
-
新增、编辑、查看详情优先采用抽屉或弹窗,不走重型嵌套表格交互
-
批量操作放在顶部工具栏,不再依赖右键菜单作为唯一入口
...
...
@@ -74,6 +74,7 @@
-
`problem_rectification`
与
`issue_rectification_archive`
列表页统一复用
`RecordWorkbench`
作为“结构化表格 + 工具栏 + 详情抽屉入口”的工作台容器
-
页面本身只负责提供 subject 对应的数据加载、统计口径、筛选 schema、行操作与批量操作
-
`problem_rectification`
主列表采用传统分页表格,不一次性加载全部数据;每次按当前视图、筛选条件、关键词和页码调用
`/record/query`
,分页参数使用
`page`
与
`page_size`
-
主列表提供“检查报告出具月份”筛选,用户选择
`YYYY-MM`
后,仅查询
`metadata.inspection_report_date`
位于该月份的记录;该筛选不使用其它检查或整改日期兜底
-
主列表支持每页
`10 / 20 / 50 / 100`
条,切换视图、修改筛选或清空筛选时回到第 1 页;表头勾选只选择当前页记录,跨页批量处理需用户逐页确认选择范围
-
主列表视图页签数量通过
`/record/count`
按各视图限制和当前筛选条件计算,表格标题区展示服务端筛选后的总数与当前页记录数
-
主列表滚动到表格区域后,表格工作台吸顶展示;标题栏、操作区、列分组和分页固定在工作台内,数据表格区域独立滚动,表头列名在数据区域内继续吸顶,折叠 / 展开列分组时保留当前表格滚动位置
...
...
@@ -90,7 +91,7 @@
1.
`integrity_risk_table/dashboard`
问题整改驾驶舱
2.
`integrity_risk_table/business_dashboard`
业务驾驶舱
问题整改分析
3.
`problem_rectification`
问题整改主列表
4.
`issue_rectification_archive`
...
...
@@ -100,8 +101,8 @@
对应推荐页面职责:
-
驾驶舱:
展示管理层首屏关注的宏观总览
,保留部门分组切换,不承接编辑
-
业务驾驶舱:展示后续
业务分析图表、责任追踪、重点问题清单与质效分布,保留部门分组切换,不承接编辑
-
驾驶舱:
面向管理层展示宏观总览和整改进度分布
,保留部门分组切换,不承接编辑
-
整改分析:展示
业务分析图表、责任追踪、重点问题清单与质效分布,保留部门分组切换,不承接编辑
-
主列表:承接查询、筛选、批量操作、详情入口
-
归档页:承接归档批次查看与明细追溯
-
外链流程页:承接填写、复核、最终审核,不与主列表混杂
...
...
@@ -146,14 +147,16 @@
## 5. 旧应用核心能力
旧应用
`problem_rectification`
列表配置中实际暴露了 5 个
操作:
当前
`problem_rectification`
列表配置暴露 8 类
操作:
1.
查看详情
2.
复制数据
3.
删除数据
4.
推送问题整改表
5.
导出问题整改表
6.
一键归档
5.
推送延期申请
6.
导出问题整改表
7.
一键归档
8.
状态锁定 / 解锁
这些操作分别对应如下业务含义:
...
...
@@ -181,21 +184,38 @@
-
记录被推送问题的
`_id`
列表到
`restriction_value`
-
将选中问题的
`metadata.fill_in_the_people`
批量更新为填报人
-
给填报人发送内部通知
-
创建 OA 待办,链接指向
`/problem_rectification_form?id=<message_record_name>`
-
创建 OA 待办,链接使用相对地址
`/problem_rectification_new/problem_rectification_flow_table?id=<message_record_name>`
,不拼接浏览器当前域名
### 5.5 推送延期申请
-
每次只允许选择 1 条问题记录,不要求该问题已生成
`department_rectification_items`
-
弹窗为本次问题级延期申请选择 1 个填写人和 1 个一级审批人,发起人为最终审批人
-
延期申请保存于
`metadata.problem_rectification_delay_request`
,与
`department_rectification_items`
平级;每条问题最多保存 1 份延期申请
-
创建
`message_reference_id`
流程主单和 OA 填写待办,流程链接使用相对地址
`/problem_rectification_new/problem_rectification_flow_table?id=<message_record_name>`
,不拼接浏览器当前域名
-
历史 OA 待办若仍使用
`/self_inspection_form?id=<message_record_name>`
,应用入口会保留原流程 ID 并重定向到上述表格处理页
-
已锁定、未选择流程人员或已有延期申请的问题不能发起
### 5.
5
导出问题整改表
### 5.
6
导出问题整改表
-
按选中问题导出 Excel
-
导出字段来自
`problem_rectification.metadata`
-
本质是把主表字段映射成报表列,不参与状态流转
### 5.
6
一键归档
### 5.
7
一键归档
-
将选中问题记录导入
`problem_rectification_archive_records`
-
再创建一条
`issue_rectification_archive`
记录,保存归档明细引用列表
-
归档的本质是生成快照,不应直接依赖原始问题记录后续继续存在
### 5.7 旧多维表格能力盘点
### 5.8 状态锁定 / 解锁
-
单行操作区按状态显示锁定或解锁图标按钮,按钮权限后续接入权限配置
-
只有整改进度汇总值严格为“已整改”且尚未锁定的记录显示“锁定”;“已优化”及其它状态不显示该按钮
-
锁定写入
`metadata.is_locked = true`
;解锁写入
`metadata.is_locked = false`
-
锁定后所有用户只能查看记录,不允许编辑、复制、删除、推送整改、保存流程内容、送审、复核或最终审批
-
解锁后恢复原有视图权限和流程角色权限
### 5.9 旧多维表格能力盘点
实际页面检查后,旧系统“问题整改”和“往期材料归档”页都运行在同一套多维表格容器上,页面共有能力包括:
...
...
@@ -264,7 +284,7 @@
3.
根据 2026 年整改驾驶舱要求,补录扩展治理字段:
-
问题后果:是否造成经济损失、对应经济损失、是否造成外部影响、处罚机构、是否涉及员工问责、问责人数
-
问题画像:问题综合严重程度、问题一级至四级分类、问题原因
-
整改质效:整改时间评估、整改证明材料质量、是否涉及制度修订、制度名称、是否涉及系统建设、系统名称、是否为往期发现问题、问题是否重复出现
、综合整改评定等级
-
整改质效:整改时间评估、整改证明材料质量、是否涉及制度修订、制度名称、是否涉及系统建设、系统名称、是否为往期发现问题、问题是否重复出现
;综合整改质效结果由系统计算,不提供人工录入
-
责任识别:主责部门、实际整改完成时间、实际检查完成日期
4.
可预先填写预计整改完成时间
5.
初始时可不指定填报人、复核人
...
...
@@ -299,7 +319,7 @@
6.
单条部门推送时,回写该问题的
`metadata.department_rectification_items`
;每个部门明细独立保存责任部门、填写人、审批人、整改字段、审批状态和待办 ID
-
写入后重新读取父问题并校验部门明细数量;后台未返回预期明细时,本次推送显示失败,不展示成功提示
7.
多选简单推送时,批量更新选中问题的
`metadata.fill_in_the_people`
、
`metadata.review_people`
8.
发送内部通知与 OA 待办,链接
指向
`/problem_rectification_new/problem_rectification_flow_table?id=<message_record_name>`
8.
发送内部通知与 OA 待办,链接
使用相对地址
`/problem_rectification_new/problem_rectification_flow_table?id=<message_record_name>`
,不拼接浏览器当前域名;问题整改推送与延期申请推送统一遵守该规则
推荐新交互:
...
...
@@ -374,6 +394,39 @@
-
最终审批行级处理,不要求本次推送所有部门都先审批通过
-
发起人编辑已经送审或审批完成的父问题、部门整改内容时,不自动重置审批状态,也不自动重新生成 OA 待办
### 7.6 问题延期申请
#### 7.6.1 发起与填写
1.
在问题整改列表中只勾选 1 条问题,点击“推送延期申请”
2.
系统展示当前问题及主记录预计完成时间,由发起人选择 1 个填写人和 1 个一级审批人
3.
问题主记录已有
`problem_rectification_delay_request`
时不能重复推送;是否存在部门整改明细不影响延期发起
4.
确认推送后创建
`message_reference_id`
,写入
`flow_mode = "delay"`
、问题 ID、填写人、一级审批人和推送人
5.
在
`metadata.problem_rectification_delay_request`
初始化延期申请:原预计完成时间取主记录当前
`the_time_of_rectification`
,填写、一级审批、最终审批状态分别初始化为“未完成 / 未审批 / 未审批”
6.
系统为填写人创建 OA 待办并发送内部通知,链接使用相对地址
`/problem_rectification_new/problem_rectification_flow_table?id=<message_record_name>`
7.
填写人进入流程页后填写
`requested_due_date`
(申请完成时间)和
`delay_reason`
(延期事由);草稿可以暂存,提交时两项均必填
#### 7.6.2 一级审批
1.
填写人提交后,系统完成填写 OA 待办,写入
`editor_status = "已完成"`
、
`submitted_at`
,并创建一级审批人 OA 待办
2.
一级审批通过时写入
`auditor_status = "已审批"`
、
`auditor_decided_at`
,并立即给推送发起人创建最终审批 OA 待办
3.
一级审批驳回时写入
`auditor_status = "已驳回"`
,把
`editor_status`
重置为“未完成”,重新给填写人创建 OA 待办
#### 7.6.3 最终审批与生效
1.
推送发起人作为最终审批人,处理已通过一级审批的问题延期申请
2.
最终审批通过时写入
`sender_status = "已审批"`
、
`sender_decided_at`
,再把
`requested_due_date`
写回问题主记录的
`the_time_of_rectification`
3.
延期申请不修改已有
`department_rectification_items`
的预计完成时间或整改审批状态
4.
最终审批驳回时写入
`sender_status = "已驳回"`
,把
`editor_status`
重置为“未完成”,重新给填写人创建 OA 待办;原预计完成时间保持不变
5.
驳回后填写人修改并重新提交时,流程再次经过一级审批和最终审批
限制与异常处理:
-
`metadata.is_locked = true`
时,禁止发起、保存、提交或审批延期申请
-
延期申请的状态和待办 ID 只写入问题主记录的嵌套对象,不覆盖部门明细的审批状态和原整改待办 ID
-
后台问题记录在提交前发生变化时,操作失败并提示刷新后重试
-
OA 待办创建、业务记录写入或内部通知发送失败时展示错误,不显示成功提示
---
## 8. 状态流转约定
...
...
@@ -402,7 +455,7 @@
-
`sender`
记录推送发起人
-
`editor`
记录填写人列表
-
`auditor`
记录审核人列表
-
`flow_mode`
记录流程模式:
`department`
表示单条问题按责任部门拆分
明细,
`batch`
表示多选问题的单填写人 / 单审批人简单推送
-
`flow_mode`
记录流程模式:
`department`
表示单条问题按责任部门拆分
整改明细,
`batch`
表示多选问题的单填写人 / 单审批人简单推送,
`delay`
表示问题级延期申请;历史
`delay_department`
仅保留页面读取兼容
-
`department_item_ids`
记录本次部门流程关联的
`department_rectification_items[].item_id`
-
`restriction_key`
默认固定为
`_id`
-
`restriction_value`
保存本次推送绑定的整改记录
`_id[]`
...
...
@@ -427,14 +480,31 @@
-
新流程页中,填写人只能维护本人负责行的整改业务字段,送审后锁定,复核驳回后恢复;复核驳回状态下对应审批人可以维护该行整改业务字段,审批通过后锁定,且审批人不能点击填写人的“送审”;流程推送人可以维护本次推送关联的父问题记录和全部部门明细整改业务字段,但不能直接编辑流程状态与待办 ID
-
推送人的内容修改保持当前审批状态不变,不触发重新送审
### 8.4 延期申请状态约定
-
每条问题主记录最多保存 1 个
`problem_rectification_delay_request`
嵌套对象,路径为
`metadata.problem_rectification_delay_request`
-
`editor_status`
使用“未完成 / 已完成”;
`auditor_status`
、
`sender_status`
使用“未审批 / 已审批 / 已驳回”
-
`progress_id_editor`
、
`progress_id_auditor`
、
`progress_id_sender`
记录延期申请各阶段 OA 待办 ID
-
最终审批通过前,
`original_due_date`
和问题主记录原
`the_time_of_rectification`
保持不变
-
最终审批通过后,以
`requested_due_date`
更新问题主记录
`the_time_of_rectification`
,嵌套对象继续保留完整审批记录
### 8.5 主记录状态锁定约定
-
`metadata.is_locked`
为 Boolean 类型,只有
`true`
表示锁定
-
锁定操作仅在父问题整改进度汇总为“已整改”时开放
-
主记录锁定优先级高于视图编辑权限和流程角色权限;锁定后详情抽屉、外链流程页和表格流程页均切换为只读
-
锁定后禁止删除、复制、再次推送以及所有保存和审批状态变更,仅保留查看与解锁
-
解锁不修改整改进度、审批状态或部门明细,只恢复原有权限规则下的可操作状态
-
当前锁定 / 解锁按钮对所有用户展示,后续在按钮层增加权限判断,不改变
`is_locked`
数据协议
---
## 9. 驾驶舱展示规则
当前
驾驶舱
能力以
`problem_rectification`
为唯一数据源,并拆分为两个菜单入口:
当前
统计
能力以
`problem_rectification`
为唯一数据源,并拆分为两个菜单入口:
-
`integrity_risk_table/dashboard`
:问题整改驾驶舱,
仅展示顶部横幅、部门分组切换与宏观总览 KPI
-
`integrity_risk_table/business_dashboard`
:
业务驾驶舱,展示原宏观总览之后的业务分析内容,并继续支持部门分组切换
-
`integrity_risk_table/dashboard`
:问题整改驾驶舱,
面向管理层展示顶部横幅、部门分组切换、宏观总览 KPI 与整改进度分布
-
`integrity_risk_table/business_dashboard`
:
问题整改分析,展示问题风险定位、责任追踪与整改质效内容,并继续支持部门分组切换;为兼容历史链接保留原路由
围绕 2026 年整改驾驶舱需求,整体统计能力分为四层:
...
...
@@ -468,11 +538,11 @@
当前实现约定:
-
两个
驾驶舱
入口顶部均保留按
`feedback_unit`
分组的轻量 tabs
-
两个
驾驶舱
入口顶部均提供“统计年份”选择器,默认“全部年份”,选择具体年份后仅统计该年份内的问题记录
-
问题整改驾驶舱的 tabs 下方
只展示宏观总览 KPI 卡片
-
业务驾驶舱的 tabs 下方展示整改进度分布、
业务风险分析、责任追踪和整改质效图表
-
两个
驾驶舱
均仅负责统计展示,不承接编辑动作
-
两个
统计
入口顶部均保留按
`feedback_unit`
分组的轻量 tabs
-
两个
统计
入口顶部均提供“统计年份”选择器,默认“全部年份”,选择具体年份后仅统计该年份内的问题记录
-
问题整改驾驶舱的 tabs 下方
展示宏观总览 KPI 卡片和整改进度分布
-
问题整改分析的 tabs 下方展示
业务风险分析、责任追踪和整改质效图表
-
两个
统计入口
均仅负责统计展示,不承接编辑动作
-
如果线上 Subject 后续把新增字段从
`text`
升级为
`select`
/
`date`
/
`boolean`
,驾驶舱统计口径无需调整,只需复用标准字段值
### 9.1.1 统计年份筛选
...
...
@@ -500,7 +570,7 @@
-
以
`rectification_progress`
为维度统计数量和占比
-
当前页面使用环形图表达,不再沿用旧版折线趋势表达
-
业务驾驶舱仅保留
整改进度分布图,不再展示单独的“进度口径说明”卡片
-
问题整改驾驶舱展示
整改进度分布图,不再展示单独的“进度口径说明”卡片
### 9.3 问题分析与风险定位层
...
...
@@ -525,7 +595,7 @@
-
X 轴:责任部门问题总数
-
Y 轴:整改完成率
-
气泡大小:逾期问题数
-
业务驾驶舱
不再展示单独的“整改时效分析”散点图
-
问题整改分析
不再展示单独的“整改时效分析”散点图
当前逾期 / 延期判定说明:
...
...
@@ -539,7 +609,7 @@
当前图表与字段映射:
-
驾驶舱
只展示最终“综合整改质效结果”的高 / 中 / 低分布,不再展示整改时间评估、整改证明材料质量、制度修订、系统建设、往期发现、重复出现等明细维度
-
问题整改分析
只展示最终“综合整改质效结果”的高 / 中 / 低分布,不再展示整改时间评估、整改证明材料质量、制度修订、系统建设、往期发现、重复出现等明细维度
-
明细维度仍在维护页录入和校验,包括:
-
`rectification_timeliness_level`
:整改时间评估,低 / 中 / 高
-
`rectification_material_quality_level`
:整改证明材料质量,低 / 中 / 高
...
...
@@ -547,8 +617,12 @@
-
`involves_system_construction`
、
`system_construction_name`
:是否涉及系统建设及系统名称
-
`is_historical_problem`
:是否为往期发现问题
-
`is_repeated_problem`
:问题是否重复出现
-
`rectification_overall_rating`
:综合整改评定等级,低 / 中 / 高
-
当前驾驶舱优先使用已维护的
`rectification_overall_rating`
;若该字段为空,则按初步规则从明细维度推导结果:两个及以上高为高;出现低则低;一个高则中。具体参与维度和最终规则后续需业务确认
-
`rectification_overall_rating`
:历史兼容字段,新建和统计均不再读取或写入
-
综合整改质效结果由系统实时计算,不写回
`rectification_overall_rating`
:
-
整改时间评估或整改证明材料质量任一项为“高”,或者四个“是否”字段任一项为“是”时,结果为“高”;高等级优先
-
未出现“高”或“是”,且整改时间评估或整改证明材料质量任一项为“低”时,结果为“低”
-
已存在至少一个有效评估维度、但未满足高或低规则时,结果为“中”
-
六个参与字段均无有效值时,不生成综合整改质效结果
---
...
...
src/pages/problem_rectification/problemRectificationDashboardService.js
View file @
3917767f
...
...
@@ -150,34 +150,29 @@ function normalizeRatingLevel(value) {
return
RATING_LEVELS
.
includes
(
text
)
?
text
:
''
;
}
function
calculateQualityRating
(
record
)
{
const
explicitRating
=
normalizeRatingLevel
(
record
.
overallRating
);
if
(
explicitRating
)
{
return
explicitRating
;
}
export
function
calculateRectificationQualityRating
(
record
)
{
const
dimensions
=
[
normalizeRatingLevel
(
record
.
timelinessLevel
),
normalizeRatingLevel
(
record
.
materialQualityLevel
),
record
.
involvesPolicyRevision
?
'高'
:
''
,
record
.
involvesSystemConstruction
?
'高'
:
''
,
record
.
isHistoricalProblem
?
'
低
'
:
''
,
record
.
isRepeatedProblem
?
'
低
'
:
''
,
record
.
isHistoricalProblem
?
'
高
'
:
''
,
record
.
isRepeatedProblem
?
'
高
'
:
''
,
].
filter
(
Boolean
);
if
(
dimensions
.
length
===
0
)
{
return
''
;
}
if
(
dimensions
.
includes
(
'低'
))
{
return
'低'
;
}
const
highCount
=
dimensions
.
filter
((
item
)
=>
item
===
'高'
).
length
;
if
(
highCount
>=
2
)
{
if
(
highCount
>=
1
)
{
return
'高'
;
}
if
(
dimensions
.
includes
(
'低'
))
{
return
'低'
;
}
return
'中'
;
}
...
...
@@ -468,7 +463,6 @@ function normalizeProblemRecord(record) {
rootCause
:
normalizeText
(
metadata
.
problem_root_cause
),
timelinessLevel
:
normalizeText
(
metadata
.
rectification_timeliness_level
),
materialQualityLevel
:
normalizeText
(
metadata
.
rectification_material_quality_level
),
overallRating
:
normalizeText
(
metadata
.
rectification_overall_rating
),
primaryResponsibleDepartment
:
extractPrimaryDepartment
(
metadata
),
responsibleDepartments
:
toArray
(
metadata
.
responsible_department
)
.
map
((
item
)
=>
normalizeText
(
item
))
...
...
@@ -500,7 +494,7 @@ function normalizeProblemRecord(record) {
return
{
...
normalizedRecord
,
qualityRating
:
calculateQualityRating
(
normalizedRecord
),
qualityRating
:
calculate
Rectification
QualityRating
(
normalizedRecord
),
};
}
...
...
src/pages/problem_rectification/problemRectificationFormService.js
View file @
3917767f
...
...
@@ -153,6 +153,29 @@ function buildReturnUrl(messageName) {
return
getProblemRectificationFormUrl
(
messageName
);
}
async
function
assertRelatedRecordsUnlocked
(
messageRecord
)
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
recordIds
=
normalizeRestrictionValue
(
metadata
.
restriction_value
);
if
(
recordIds
.
length
===
0
)
{
return
;
}
const
lockedResult
=
await
queryRecords
({
subject
:
metadata
.
subject_name
||
'problem_rectification'
,
page
:
0
,
page_size
:
1
,
mode
:
'lean'
,
filter
:
{
_id
:
{
$in
:
recordIds
},
'metadata.is_locked'
:
true
,
},
});
if
((
lockedResult
?.
records
||
[]).
length
>
0
)
{
throw
new
Error
(
'当前流程包含已锁定的问题整改记录,不允许继续修改或审批。'
);
}
}
function
resolveCurrentStep
(
messageRecord
)
{
const
metadata
=
messageRecord
?.
metadata
||
{};
const
editorStatus
=
metadata
.
editor_status
||
DEFAULT_STATUS
;
...
...
@@ -280,6 +303,7 @@ export async function markEditorComplete({
currentUser
,
auditors
,
})
{
await
assertRelatedRecordsUnlocked
(
messageRecord
);
const
metadata
=
messageRecord
?.
metadata
||
{};
const
messageName
=
messageRecord
?.
name
||
messageRecord
?.
_id
;
const
returnUrl
=
buildReturnUrl
(
messageName
);
...
...
@@ -324,6 +348,7 @@ export async function approveByAuditor({
currentUser
,
sender
,
})
{
await
assertRelatedRecordsUnlocked
(
messageRecord
);
const
metadata
=
messageRecord
?.
metadata
||
{};
const
messageName
=
messageRecord
?.
name
||
messageRecord
?.
_id
;
const
returnUrl
=
buildReturnUrl
(
messageName
);
...
...
@@ -368,6 +393,7 @@ export async function rejectByAuditor({
currentUser
,
editors
,
})
{
await
assertRelatedRecordsUnlocked
(
messageRecord
);
const
metadata
=
messageRecord
?.
metadata
||
{};
const
messageName
=
messageRecord
?.
name
||
messageRecord
?.
_id
;
const
returnUrl
=
buildReturnUrl
(
messageName
);
...
...
@@ -411,6 +437,7 @@ export async function rejectByAuditor({
export
async
function
approveBySender
({
messageRecord
,
})
{
await
assertRelatedRecordsUnlocked
(
messageRecord
);
const
metadata
=
messageRecord
?.
metadata
||
{};
await
batchUpdateRecords
({
...
...
@@ -437,6 +464,7 @@ export async function rejectBySender({
currentUser
,
editors
,
})
{
await
assertRelatedRecordsUnlocked
(
messageRecord
);
const
metadata
=
messageRecord
?.
metadata
||
{};
const
messageName
=
messageRecord
?.
name
||
messageRecord
?.
_id
;
const
returnUrl
=
buildReturnUrl
(
messageName
);
...
...
src/pages/problem_rectification/problemRectificationListService.js
View file @
3917767f
...
...
@@ -5,7 +5,10 @@ import {
loadSubject
,
queryRecords
,
}
from
'@/services/DataService'
;
import
{
DEPARTMENT_GROUPS
}
from
'./problemRectificationDashboardService'
;
import
{
calculateRectificationQualityRating
,
DEPARTMENT_GROUPS
,
}
from
'./problemRectificationDashboardService'
;
import
{
buildViewColumns
,
checkViewCondition
,
...
...
@@ -25,8 +28,11 @@ export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification';
export
const
PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS
=
[
10
,
20
,
50
,
100
];
export
const
DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE
=
20
;
export
const
DEPARTMENT_RECTIFICATION_FIELD
=
'department_rectification_items'
;
export
const
DELAY_REQUEST_FIELD
=
'problem_rectification_delay_request'
;
export
const
FLOW_MODE_DEPARTMENT
=
'department'
;
export
const
FLOW_MODE_BATCH
=
'batch'
;
export
const
FLOW_MODE_DELAY
=
'delay'
;
export
const
FLOW_MODE_DELAY_DEPARTMENT
=
'delay_department'
;
export
const
PROBLEM_RECTIFICATION_FLOW_TABLE_PATH
=
'/problem_rectification_flow_table'
;
export
const
EDITOR_STATUS_PENDING
=
'未完成'
;
export
const
EDITOR_STATUS_COMPLETED
=
'已完成'
;
...
...
@@ -39,7 +45,6 @@ const FILTER_FIELD_PATHS = {
progress
:
'metadata.rectification_progress'
,
auditStatus
:
'metadata.audit_status'
,
severityLevel
:
'metadata.problem_severity_level'
,
overallRating
:
'metadata.rectification_overall_rating'
,
primaryResponsibleDepartment
:
'metadata.primary_responsible_department'
,
problemFrom
:
'metadata.problem_from'
,
rootCause
:
'metadata.problem_root_cause'
,
...
...
@@ -66,7 +71,6 @@ const WORKBENCH_PRIORITY_CREATE_FIELD_NAMES = [
'system_construction_name'
,
'is_historical_problem'
,
'is_repeated_problem'
,
'rectification_overall_rating'
,
'actual_rectification_completed_at'
,
'inspection_completed_at'
,
'primary_responsible_department'
,
...
...
@@ -151,13 +155,7 @@ export function getProblemRectificationFlowTableUrl(messageName) {
const
query
=
messageName
?
`?id=
${
encodeURIComponent
(
messageName
)}
`
:
''
;
const
basePath
=
String
(
PROBLEM_RECTIFICATION_APP_BASE_PATH
||
'/'
)
.
replace
(
/
\/
+$/
,
''
);
const
appPath
=
`
${
basePath
}${
PROBLEM_RECTIFICATION_FLOW_TABLE_PATH
}${
query
}
`
;
if
(
typeof
window
===
'undefined'
)
{
return
appPath
;
}
return
`
${
window
.
location
.
origin
}${
appPath
}
`
;
return
`
${
basePath
}${
PROBLEM_RECTIFICATION_FLOW_TABLE_PATH
}${
query
}
`
;
}
export
function
getProblemRectificationFormUrl
(
messageName
)
{
...
...
@@ -189,6 +187,7 @@ export function normalizeDepartmentRectificationItems(record) {
return
toArray
(
parsedItems
)
.
filter
((
item
)
=>
item
&&
typeof
item
===
'object'
)
.
map
((
item
,
index
)
=>
({
...
item
,
item_id
:
normalizeText
(
item
.
item_id
)
||
makeDepartmentItemId
(
record
,
item
.
responsible_department
,
index
),
responsible_department
:
normalizeText
(
item
.
responsible_department
),
editor
:
normalizeUserId
(
item
.
editor
),
...
...
@@ -210,6 +209,56 @@ export function normalizeDepartmentRectificationItems(record) {
}));
}
export
function
normalizeDelayRequest
(
value
)
{
let
parsedValue
=
value
;
if
(
typeof
value
===
'string'
)
{
try
{
parsedValue
=
JSON
.
parse
(
value
);
}
catch
(
error
)
{
return
null
;
}
}
if
(
!
parsedValue
||
typeof
parsedValue
!==
'object'
||
Array
.
isArray
(
parsedValue
))
{
return
null
;
}
const
hasRequestData
=
[
parsedValue
.
flow_record_id
,
parsedValue
.
editor
,
parsedValue
.
auditor
,
parsedValue
.
sender
,
parsedValue
.
requested_due_date
,
parsedValue
.
delay_reason
,
parsedValue
.
progress_id_editor
,
parsedValue
.
submitted_at
,
].
some
(
Boolean
);
if
(
!
hasRequestData
)
{
return
null
;
}
return
{
...
parsedValue
,
original_due_date
:
parsedValue
.
original_due_date
||
null
,
requested_due_date
:
parsedValue
.
requested_due_date
||
null
,
delay_reason
:
normalizeText
(
parsedValue
.
delay_reason
),
editor
:
normalizeUserId
(
parsedValue
.
editor
),
auditor
:
normalizeUserId
(
parsedValue
.
auditor
),
sender
:
normalizeUserId
(
parsedValue
.
sender
),
editor_status
:
normalizeText
(
parsedValue
.
editor_status
)
||
EDITOR_STATUS_PENDING
,
auditor_status
:
normalizeText
(
parsedValue
.
auditor_status
)
||
AUDIT_STATUS_PENDING
,
sender_status
:
normalizeText
(
parsedValue
.
sender_status
)
||
AUDIT_STATUS_PENDING
,
progress_id_editor
:
parsedValue
.
progress_id_editor
||
''
,
progress_id_auditor
:
parsedValue
.
progress_id_auditor
||
''
,
progress_id_sender
:
parsedValue
.
progress_id_sender
||
''
,
flow_record_id
:
parsedValue
.
flow_record_id
||
''
,
submitted_at
:
parsedValue
.
submitted_at
||
null
,
auditor_decided_at
:
parsedValue
.
auditor_decided_at
||
null
,
sender_decided_at
:
parsedValue
.
sender_decided_at
||
null
,
};
}
export
function
buildDepartmentAssignmentsFromRecord
(
record
)
{
const
metadata
=
record
?.
metadata
||
record
?.
raw
?.
metadata
||
{};
const
departments
=
uniqueTruthy
(
...
...
@@ -449,6 +498,19 @@ function buildKeywordFilter(keyword, columns = []) {
};
}
function
buildInspectionReportMonthFilter
(
month
)
{
const
normalizedMonth
=
normalizeText
(
month
);
if
(
!
/^
\d{4}
-
(
0
[
1-9
]
|1
[
0-2
])
$/
.
test
(
normalizedMonth
))
{
return
{};
}
return
{
'metadata.inspection_report_date'
:
{
$regex
:
`^
${
normalizedMonth
}
(?:-|$)`
,
},
};
}
function
buildFocusFlagFilter
(
focusFlag
)
{
if
(
focusFlag
===
'consequence'
)
{
return
{
...
...
@@ -465,7 +527,7 @@ function buildFocusFlagFilter(focusFlag) {
}
if
(
focusFlag
===
'low_rating'
)
{
return
{
'metadata.rectification_overall_rating'
:
'低'
}
;
return
buildCalculatedQualityRatingFilter
(
'低'
)
;
}
if
(
focusFlag
===
'repeated_or_historical'
)
{
...
...
@@ -496,6 +558,77 @@ function buildFocusFlagFilter(focusFlag) {
return
{};
}
function
buildCalculatedQualityRatingFilter
(
rating
)
{
if
(
!
LEVEL_ORDER
.
includes
(
rating
))
{
return
{};
}
const
normalizedField
=
(
fieldName
)
=>
({
$trim
:
{
input
:
{
$ifNull
:
[
`$metadata.
${
fieldName
}
`
,
''
]
}
},
});
const
affirmativeField
=
(
fieldName
)
=>
({
$in
:
[
`$metadata.
${
fieldName
}
`
,
AFFIRMATIVE_FILTER_VALUES
],
});
const
timelinessLevel
=
normalizedField
(
'rectification_timeliness_level'
);
const
materialQualityLevel
=
normalizedField
(
'rectification_material_quality_level'
);
const
involvesPolicyRevision
=
affirmativeField
(
'involves_policy_revision'
);
const
involvesSystemConstruction
=
affirmativeField
(
'involves_system_construction'
);
const
isHistoricalProblem
=
affirmativeField
(
'is_historical_problem'
);
const
isRepeatedProblem
=
affirmativeField
(
'is_repeated_problem'
);
const
hasLowDimension
=
{
$or
:
[
{
$eq
:
[
timelinessLevel
,
'低'
]
},
{
$eq
:
[
materialQualityLevel
,
'低'
]
},
],
};
const
highDimensionCount
=
{
$add
:
[
{
$cond
:
[{
$eq
:
[
timelinessLevel
,
'高'
]
},
1
,
0
]
},
{
$cond
:
[{
$eq
:
[
materialQualityLevel
,
'高'
]
},
1
,
0
]
},
{
$cond
:
[
involvesPolicyRevision
,
1
,
0
]
},
{
$cond
:
[
involvesSystemConstruction
,
1
,
0
]
},
{
$cond
:
[
isHistoricalProblem
,
1
,
0
]
},
{
$cond
:
[
isRepeatedProblem
,
1
,
0
]
},
],
};
const
hasValidDimension
=
{
$or
:
[
{
$in
:
[
timelinessLevel
,
LEVEL_ORDER
]
},
{
$in
:
[
materialQualityLevel
,
LEVEL_ORDER
]
},
involvesPolicyRevision
,
involvesSystemConstruction
,
isHistoricalProblem
,
isRepeatedProblem
,
],
};
if
(
rating
===
'低'
)
{
return
{
$expr
:
{
$and
:
[{
$eq
:
[
highDimensionCount
,
0
]
},
hasLowDimension
],
},
};
}
if
(
rating
===
'高'
)
{
return
{
$expr
:
{
$gte
:
[
highDimensionCount
,
1
],
},
};
}
return
{
$expr
:
{
$and
:
[
{
$eq
:
[
highDimensionCount
,
0
]
},
{
$not
:
[
hasLowDimension
]
},
hasValidDimension
,
],
},
};
}
export
function
buildProblemRectificationQueryFilter
(
filters
=
{},
gridView
,
subject
)
{
const
columns
=
buildViewColumnsForList
(
gridView
,
subject
);
const
uiFieldClauses
=
Object
.
entries
(
FILTER_FIELD_PATHS
).
map
(([
filterKey
,
fieldPath
])
=>
{
...
...
@@ -511,6 +644,8 @@ export function buildProblemRectificationQueryFilter(filters = {}, gridView, sub
buildRestrictionsFilter
(
gridView
?.
settings
?.
restrictions
),
buildViewConditionsFilter
(
gridView
?.
settings
?.
filters
),
...
uiFieldClauses
,
buildInspectionReportMonthFilter
(
filters
?.
inspectionReportMonth
),
buildCalculatedQualityRatingFilter
(
filters
?.
overallRating
),
buildFocusFlagFilter
(
filters
?.
focusFlag
),
buildKeywordFilter
(
filters
?.
keyword
,
columns
),
]);
...
...
@@ -572,12 +707,13 @@ export function normalizeProblemRectificationRecord(record) {
const
isOverdue
=
dueDate
&&
dueDate
.
isValid
()
?
dueDate
.
isBefore
(
now
,
'day'
)
&&
!
isFixed
:
false
;
return
{
const
normalizedRecord
=
{
id
:
record
?.
_id
||
record
?.
name
||
''
,
name
:
record
?.
name
||
''
,
title
:
normalizeText
(
record
?.
title
)
||
normalizeText
(
metadata
.
project_name
),
projectName
:
normalizeText
(
metadata
.
project_name
)
||
'-'
,
inspectUnit
:
normalizeText
(
metadata
.
inspect_the_implementing_unit
),
inspectionReportDate
:
normalizeText
(
metadata
.
inspection_report_date
),
progress
:
normalizedProgress
,
feedbackUnit
:
normalizeText
(
metadata
.
feedback_unit
),
auditStatus
:
summarizedAuditStatus
,
...
...
@@ -594,7 +730,6 @@ export function normalizeProblemRectificationRecord(record) {
rootCause
:
normalizeText
(
metadata
.
problem_root_cause
),
timelinessLevel
:
normalizeText
(
metadata
.
rectification_timeliness_level
),
materialQualityLevel
:
normalizeText
(
metadata
.
rectification_material_quality_level
),
overallRating
:
normalizeText
(
metadata
.
rectification_overall_rating
),
primaryResponsibleDepartment
:
normalizeText
(
metadata
.
primary_responsible_department
),
hasEconomicLoss
,
hasEconomicLossLabel
:
normalizeFlagText
(
metadata
.
has_economic_loss
),
...
...
@@ -635,10 +770,17 @@ export function normalizeProblemRectificationRecord(record) {
dueDateLabel
:
formatDateText
(
summarizedDueDate
)
||
(
hasDepartmentItems
?
'见部门明细'
:
'-'
),
departmentItems
,
hasDepartmentItems
,
delayRequest
:
normalizeDelayRequest
(
metadata
[
DELAY_REQUEST_FIELD
]),
isFixed
,
isLocked
:
metadata
.
is_locked
===
true
,
isOverdue
,
raw
:
record
,
};
return
{
...
normalizedRecord
,
overallRating
:
calculateRectificationQualityRating
(
normalizedRecord
),
};
}
export
async
function
loadProblemRectificationListData
({
...
...
@@ -738,6 +880,7 @@ export function getRecordCellText(record, column) {
export
function
buildProblemRectificationConfiguredListView
(
records
,
filters
,
gridView
,
subject
)
{
const
nextKeyword
=
filters
?.
keyword
||
''
;
const
nextInspectionReportMonth
=
normalizeText
(
filters
?.
inspectionReportMonth
);
const
nextProgress
=
filters
?.
progress
||
''
;
const
nextAuditStatus
=
filters
?.
auditStatus
||
''
;
const
nextSeverityLevel
=
filters
?.
severityLevel
||
''
;
...
...
@@ -751,6 +894,14 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
const
searchableFields
=
columns
.
map
((
column
)
=>
column
.
field
);
const
filteredRecords
=
records
.
filter
((
record
)
=>
{
if
(
nextInspectionReportMonth
&&
!
record
.
inspectionReportDate
.
startsWith
(
`
${
nextInspectionReportMonth
}
-`
)
&&
record
.
inspectionReportDate
!==
nextInspectionReportMonth
)
{
return
false
;
}
const
rawRecord
=
record
.
raw
||
record
;
if
(
!
recordMatchesRestrictions
(
rawRecord
,
gridView
?.
settings
?.
restrictions
))
{
...
...
@@ -1002,7 +1153,13 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) {
const
columns
=
buildViewColumnsForList
(
gridView
,
subject
)
.
filter
((
column
)
=>
column
.
field
!==
'title'
)
.
filter
((
column
)
=>
column
.
field
?.
startsWith
(
'metadata.'
))
.
filter
((
column
)
=>
column
.
fieldDef
?.
name
);
.
filter
((
column
)
=>
column
.
fieldDef
?.
name
)
.
filter
(
(
column
)
=>
!
[
'rectification_overall_rating'
,
'is_locked'
,
DELAY_REQUEST_FIELD
].
includes
(
column
.
fieldDef
.
name
,
),
);
const
seen
=
new
Set
();
const
fields
=
columns
.
filter
((
column
)
=>
{
...
...
src/services/subjectsDefinition.md
View file @
3917767f
...
...
@@ -103,7 +103,7 @@ type Record = {
一条记录表示一条待整改问题主记录,是本模块最核心的数据对象。
当前字段定义来自线上 Subject 配置
,共 43 个字段
。字段值写入时均放入
`metadata`
。
当前字段定义来自线上 Subject 配置。字段值写入时均放入
`metadata`
。
| 字段标签 | 字段别名 | 字段类型 | 默认值 | 是否主键 | 是否多值 | 是否必填 | 可搜索 | 可聚合 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
...
...
@@ -151,6 +151,8 @@ type Record = {
| 实际检查完成日期 |
`inspection_completed_at`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 主责部门 |
`primary_responsible_department`
| text |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 部门整改明细 |
`department_rectification_items`
| object |
`[]`
| 否 | 是 | 否 | 否 | 否 |
| 延期申请 |
`problem_rectification_delay_request`
| subject |
`null`
| 否 | 否 | 否 | 否 | 否 |
| 状态锁定 |
`is_locked`
| boolean |
`false`
| 否 | 否 | 否 | 否 | 否 |
### 3.1.1 select 字段选项
...
...
@@ -175,6 +177,8 @@ type Record = {
-
其余
`select`
类型的可选值必须以线上 Subject 配置中的
`settings.options`
为准,禁止在页面内硬编码
-
`problem_discovery_situation`
曾在历史文档和归档对象中出现,但本次主对象字段定义未包含;新页面只应在 Subject View 或线上字段配置存在时展示,不能作为
`problem_rectification`
固定字段依赖
-
`department_rectification_items`
用于新转发流程的部门级整改明细。单条问题即使只有 1 个责任部门,也生成 1 条明细;转发后
`rectification_progress`
、
`rectification_situation`
、
`the_time_of_rectification`
、
`rectification_proof_materials`
、
`audit_status`
、
`final_audit_status`
以明细为事实来源,父字段仅做兼容汇总展示
-
`problem_rectification_delay_request`
为问题主记录的单次延期申请嵌套对象,与
`department_rectification_items`
平级;发起延期不要求已有部门整改明细
-
`is_locked`
为问题整改主记录的状态锁定标记;仅整改进度汇总值为
`已整改`
的记录允许写入
`true`
,锁定后所有业务字段和流程状态均不可修改,只有解锁操作可以将其写回
`false`
-
部门明细的填写人只能编辑自己负责明细的整改业务字段,送审后(
`editor_status = "已完成"`
)不再允许编辑;审批人审批通过后(
`audit_status = "已审批"`
)不再允许编辑,审批驳回后(
`audit_status = "已驳回"`
)审批人可以编辑该明细的整改业务字段,填写人也恢复编辑。审批人恢复的只是业务字段编辑权限,不获得填写人的送审权限。流程推送人可以编辑本次推送关联的父问题记录及全部部门明细整改业务字段。审批状态、待办 ID、明细 ID 等流程控制字段不作为普通表单字段开放
-
推送人修改父问题记录或部门明细时,不自动重置该明细已经形成的审批状态,也不自动重新生成流程待办
...
...
@@ -188,6 +192,7 @@ type Record = {
| 审批人 |
`auditor`
| user | 是 | 保存用户
`_id`
,每条明细 1 个审批人 |
| 整改进度 |
`rectification_progress`
| select/text | 否 | 填写人维护,优先使用主对象整改进度选项 |
| 整改情况 |
`rectification_situation`
| long_text | 否 | 填写人维护 |
| 延期事由(兼容) |
`delay_reason`
| long_text | 否 | 已存在的明细字段;新延期流程改为写入问题主记录平级嵌套对象的同名字段,本字段仅保留兼容数据 |
| 预计整改完成时间 |
`the_time_of_rectification`
| date | 否 | 填写人维护,提交时保存 ISO 8601 |
| 整改证明材料 |
`rectification_proof_materials`
| file | 否 | 填写人上传,按部门明细独立保存 |
| 填写状态 |
`editor_status`
| text | 否 |
`未完成`
、
`已完成`
|
...
...
@@ -199,7 +204,39 @@ type Record = {
| 更新人 |
`updated_by`
| user | 否 | 最近更新用户
`_id`
|
| 更新时间 |
`updated_at`
| date | 否 | 最近更新时间 |
### 3.1.4 metadata 示例
### 3.1.4 `problem_rectification_delay_request` 嵌套对象
-
**subject**
:
`problem_rectification_delay_request`
-
**父级路径**
:
`metadata.problem_rectification_delay_request`
| 字段标签 | 字段别名 | 字段类型 | 是否必填 | 说明 |
| --- | --- | --- | --- | --- |
| 原预计完成时间 |
`original_due_date`
| date | 否 | 发起申请时从问题主记录
`the_time_of_rectification`
复制,保存 ISO 8601 |
| 申请完成时间 |
`requested_due_date`
| date | 否 | 填写人填写;提交申请时必填并保存 ISO 8601 |
| 延期事由 |
`delay_reason`
| long_text | 否 | 填写人填写;提交申请时必填 |
| 填写人 |
`editor`
| user | 否 | 保存推送时选择的填写人
`_id`
|
| 一级审批人 |
`auditor`
| user | 否 | 保存推送时选择的一级审批人
`_id`
|
| 最终审批人 |
`sender`
| user | 否 | 保存延期申请推送人
`_id`
|
| 填写状态 |
`editor_status`
| select/text | 否 |
`未完成`
、
`已完成`
|
| 一级审批状态 |
`auditor_status`
| select/text | 否 |
`未审批`
、
`已审批`
、
`已驳回`
|
| 最终审批状态 |
`sender_status`
| select/text | 否 |
`未审批`
、
`已审批`
、
`已驳回`
|
| 填写待办 ID |
`progress_id_editor`
| text | 否 | 当前延期申请填写 OA 待办 ID |
| 一级审批待办 ID |
`progress_id_auditor`
| text | 否 | 当前延期申请一级审批 OA 待办 ID |
| 最终审批待办 ID |
`progress_id_sender`
| text | 否 | 当前延期申请最终审批 OA 待办 ID |
| 流程主单 ID |
`flow_record_id`
| text | 否 | 对应
`message_reference_id`
的
`name`
或
`_id`
|
| 提交时间 |
`submitted_at`
| date | 否 | 填写人最近一次提交时间 |
| 一级审批时间 |
`auditor_decided_at`
| date | 否 | 一级审批人最近一次审批时间 |
| 最终审批时间 |
`sender_decided_at`
| date | 否 | 最终审批人最近一次审批时间 |
写入约定:
-
延期申请发起时只初始化问题主记录的嵌套对象和填写人 OA 待办,不修改主记录
`the_time_of_rectification`
-
`requested_due_date`
、
`delay_reason`
在保存草稿时可以为空,在提交申请时由前端强制校验必填
-
一级审批或最终审批驳回后,
`editor_status`
重置为
`未完成`
,并重新创建填写人 OA 待办
-
只有
`sender_status = "已审批"`
时,才把
`requested_due_date`
写回问题主记录
`the_time_of_rectification`
-
同一问题主记录已有该嵌套对象时,不允许再次创建另一份延期申请
### 3.1.5 metadata 示例
```
json
{
...
...
@@ -326,7 +363,7 @@ type Record = {
-
`restriction_value`
保存被推送整改记录的
`_id[]`
-
`reference_record_id`
与
`restriction_value`
保持一致,作为显式的“关联记录 ID”冗余字段
-
`subject_name`
保存被推送记录所属对象名,当前整改流程固定为
`problem_rectification`
-
`flow_mode`
区分
新旧流程:
`department`
表示单条问题按责任部门生成明细行,
`batch`
表示多选问题的单填写人 / 单审批人简单推送;历史数据可能为空
-
`flow_mode`
区分
流程:
`department`
表示单条问题按责任部门生成整改明细行,
`batch`
表示多选问题的单填写人 / 单审批人简单推送,
`delay`
表示问题级延期申请;历史
`delay_department`
值仅保留页面识别兼容
-
`department_item_ids`
保存本次部门流程涉及的
`department_rectification_items[].item_id`
-
`progress_id_editor`
、
`progress_id_auditor`
、
`progress_id_sender`
用于记录每一步用户确认动作生成的流程 ID,用来区分当前流程推进到了哪一步
-
`editor_status`
:当前项目统一使用
`未完成`
、
`已完成`
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment