Commit 3917767f authored by David Yang's avatar David Yang

feat: 问题整改转发

parent 9b9085cc
{ {
"name": "problem_rectification_new", "name": "problem_rectification_new",
"version": "1.0.4", "version": "1.0.6",
"build": "abc", "build": "abc",
"description": "merge master", "description": "merge master",
"date": "2025-07-14 19:27:20", "date": "2025-07-14 19:27:20",
......
...@@ -2,6 +2,19 @@ import '@/global.less'; ...@@ -2,6 +2,19 @@ import '@/global.less';
import '@/tailwind.css'; import '@/tailwind.css';
import { defineApp } from 'umi'; 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() { export async function getInitialState() {
return { return {
name: 'project_manager_xy', name: 'project_manager_xy',
......
...@@ -737,6 +737,7 @@ export default function RecordWorkbench({ ...@@ -737,6 +737,7 @@ export default function RecordWorkbench({
</Select> </Select>
) : ( ) : (
<Input <Input
type={field.type === 'month' ? 'month' : 'text'}
value={filters[field.key] || ''} value={filters[field.key] || ''}
onChange={(event) => onFiltersChange?.(field.key, event.target.value)} onChange={(event) => onFiltersChange?.(field.key, event.target.value)}
placeholder={field.placeholder} placeholder={field.placeholder}
......
...@@ -102,7 +102,7 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName) ...@@ -102,7 +102,7 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName)
}, [recordName, refreshKey, subjectName]); }, [recordName, refreshKey, subjectName]);
const handleFieldUpdate = async (fieldPath, newValue, fieldDef) => { const handleFieldUpdate = async (fieldPath, newValue, fieldDef) => {
if (readonly || !editableRecord) { if (readonly || remoteRecord?.metadata?.is_locked === true || !editableRecord) {
return; return;
} }
...@@ -122,7 +122,12 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName) ...@@ -122,7 +122,12 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName)
}; };
const isFieldDisabled = (fieldPath, currentView) => { 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 { return {
......
...@@ -124,6 +124,8 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -124,6 +124,8 @@ const RecordDetailDrawer = (props, ref) => {
); );
const activeView = detailViews.find((view) => view.name === activeTabKey) || detailViews[0]; 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 drawerWidth = isFullscreen ? '100vw' : typeof width === 'number' ? `${width}px` : width;
const disabledFieldSet = useMemo( const disabledFieldSet = useMemo(
() => () =>
...@@ -214,22 +216,31 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -214,22 +216,31 @@ const RecordDetailDrawer = (props, ref) => {
{error} {error}
</div> </div>
) : ( ) : (
<SubjectViewContent <div className="space-y-4">
view={activeView} {isRecordLocked ? (
subject={subject} <div className="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">
editableRecord={editableRecord} 当前问题整改记录已锁定,仅可查看。如需修改,请先在问题清单中解锁。
onFieldUpdate={handleFieldUpdate} </div>
onDataChange={onDataChange} ) : null}
readonly={readonly} <SubjectViewContent
isFieldDisabled={isDrawerFieldDisabled} view={activeView}
/> subject={subject}
editableRecord={editableRecord}
onFieldUpdate={handleFieldUpdate}
onDataChange={onDataChange}
readonly={effectiveReadonly}
isFieldDisabled={isDrawerFieldDisabled}
/>
</div>
)} )}
</main> </main>
<footer className="border-t border-white/70 bg-white/82 px-6 py-4 backdrop-blur"> <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="flex items-center justify-between gap-3">
<div className="text-sm text-zinc-500"> <div className="text-sm text-zinc-500">
长文本、附件和状态类字段支持在详情面板内直接维护 {isRecordLocked
? '当前记录已锁定,所有业务字段均为只读'
: '长文本、附件和状态类字段支持在详情面板内直接维护'}
</div> </div>
<Button outline onClick={closeDrawer}> <Button outline onClick={closeDrawer}>
关闭 关闭
......
...@@ -27,7 +27,7 @@ const menuGroups = [ ...@@ -27,7 +27,7 @@ const menuGroups = [
icon: HomeModernIcon, icon: HomeModernIcon,
}, },
{ {
name: '业务驾驶舱', name: '整改分析',
path: '/integrity_risk_table/business_dashboard', path: '/integrity_risk_table/business_dashboard',
icon: ChartPieIcon, icon: ChartPieIcon,
}, },
......
...@@ -711,10 +711,11 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' } ...@@ -711,10 +711,11 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
? summary.focusIssueGroups?.[activeFocusIssueType.key] || [] ? summary.focusIssueGroups?.[activeFocusIssueType.key] || []
: []; : [];
const isBusinessDashboard = dashboardMode === 'business'; const isBusinessDashboard = dashboardMode === 'business';
const dashboardTitle = isBusinessDashboard ? '业务驾驶舱' : '问题整改驾驶舱'; const dashboardEyebrow = isBusinessDashboard ? 'Rectification Analysis' : 'Leadership Dashboard';
const dashboardTitle = isBusinessDashboard ? '问题整改分析' : '问题整改驾驶舱';
const dashboardSubtitle = isBusinessDashboard const dashboardSubtitle = isBusinessDashboard
? '围绕问题风险定位、部门责任追踪和整改质效评估,沉淀业务分析与督办依据。' ? '围绕问题风险定位、部门责任追踪和整改质效评估,形成业务分析与督办依据。'
: '围绕整改完成进度、问题风险定位、部门责任压实和整改质效评估,统一展示今年问题整改全景。'; : '面向管理层集中展示问题总量、整改进度、闭环情况和逾期风险,快速掌握整改全景。';
const yearLabel = activeYear === 'all' ? '全部年份' : `${activeYear} 年`; const yearLabel = activeYear === 'all' ? '全部年份' : `${activeYear} 年`;
return ( return (
...@@ -725,7 +726,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' } ...@@ -725,7 +726,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<div className="relative flex flex-wrap items-end justify-between gap-6"> <div className="relative flex flex-wrap items-end justify-between gap-6">
<div className="max-w-4xl"> <div className="max-w-4xl">
<div className="text-xs font-semibold uppercase tracking-[0.28em] text-white/75"> <div className="text-xs font-semibold uppercase tracking-[0.28em] text-white/75">
Integrity Risk Dashboard {dashboardEyebrow}
</div> </div>
<h1 className="mt-3 text-3xl font-semibold tracking-tight md:text-[2.55rem]"> <h1 className="mt-3 text-3xl font-semibold tracking-tight md:text-[2.55rem]">
{dashboardTitle} {dashboardTitle}
...@@ -809,69 +810,69 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' } ...@@ -809,69 +810,69 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
{!loading && !dashboardData.isEmpty && !activeScope.isEmpty ? ( {!loading && !dashboardData.isEmpty && !activeScope.isEmpty ? (
<> <>
{!isBusinessDashboard ? ( {!isBusinessDashboard ? (
<section className="space-y-5"> <>
<SectionHeading <section className="space-y-5">
eyebrow="Overview" <SectionHeading
title="宏观总览" eyebrow="Overview"
description="先看整体整改健康度,快速判断问题总量、闭环情况和逾期风险。" title="宏观总览"
/> description="先看整体整改健康度,快速判断问题总量、闭环情况和逾期风险。"
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
<StatCard
label="问题总数"
value={formatCount(summary.totalCount)}
hint="当前分组内全部问题记录"
tone="rose"
/>
<StatCard
label="已完成整改数"
value={formatCount(summary.fixedCount)}
hint="整改进度为已整改或已优化"
tone="emerald"
/>
<StatCard
label="整改完成率"
value={formatPercent(summary.completionRate)}
hint="已完成整改数 / 总问题数"
tone="sky"
/>
<StatCard
label="逾期未完成数"
value={formatCount(summary.overdueCount)}
hint="截止今天已超期且未闭环"
tone="amber"
/>
<StatCard
label="逾期率"
value={formatPercent(summary.overdueRate)}
hint="逾期未完成数 / 总问题数"
tone="amber"
/> />
</div> <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
</section> <StatCard
) : null} label="问题总数"
value={formatCount(summary.totalCount)}
hint="当前分组内全部问题记录"
tone="rose"
/>
<StatCard
label="已完成整改数"
value={formatCount(summary.fixedCount)}
hint="整改进度为已整改或已优化"
tone="emerald"
/>
<StatCard
label="整改完成率"
value={formatPercent(summary.completionRate)}
hint="已完成整改数 / 总问题数"
tone="sky"
/>
<StatCard
label="逾期未完成数"
value={formatCount(summary.overdueCount)}
hint="截止今天已超期且未闭环"
tone="amber"
/>
<StatCard
label="逾期率"
value={formatPercent(summary.overdueRate)}
hint="逾期未完成数 / 总问题数"
tone="amber"
/>
</div>
</section>
{isBusinessDashboard ? (
<>
<section className="space-y-5"> <section className="space-y-5">
<SectionHeading <SectionHeading
eyebrow="Progress" eyebrow="Progress"
title="整改进度" title="整改进度"
description="按整改进度统计分布与口径,辅助判断当前积压状态和督办入口。" description="按整改进度统计分布与口径,帮助管理层判断当前积压状态和重点督办方向。"
/> />
<div className="grid gap-6"> <Panel
<Panel title="整改进度分布"
title="整改进度分布" description="按整改进度统计数量与占比,直观看当前积压主要落在哪个状态。"
description="按整改进度统计数量与占比,直观看当前积压主要落在哪个状态。" >
> {summary.progressDistribution.length > 0 ? (
{summary.progressDistribution.length > 0 ? ( <ReactECharts option={progressOption} style={{ height: 340 }} />
<ReactECharts option={progressOption} style={{ height: 340 }} /> ) : (
) : ( <EmptyBlock title="暂无整改进度分布数据" />
<EmptyBlock title="暂无整改进度分布数据" /> )}
)} </Panel>
</Panel>
</div>
</section> </section>
</>
) : null}
{isBusinessDashboard ? (
<>
<section className="space-y-5"> <section className="space-y-5">
<SectionHeading <SectionHeading
eyebrow="Risk" eyebrow="Risk"
...@@ -997,11 +998,11 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' } ...@@ -997,11 +998,11 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<SectionHeading <SectionHeading
eyebrow="Quality" eyebrow="Quality"
title="审计整改质效" title="审计整改质效"
description="驾驶舱只展示综合整改质效结果,具体评估维度在维护页补录和校验。" description="分析页只展示综合整改质效结果,具体评估维度在维护页补录和校验。"
/> />
<Panel <Panel
title="综合整改质效结果" title="综合整改质效结果"
description="按高、中、低展示最终整改质效等级;暂按“两个及以上高为高、出现低则低、一个高则中”的初步规则计算,具体维度和规则后续确认。" description="按六项整改评估字段自动计算并展示高、中、低分布;任一负向维度为低,至少两个正向维度为高,其余有效结果为中。"
> >
{summary.qualityRatingDistribution.length > 0 ? ( {summary.qualityRatingDistribution.length > 0 ? (
<ReactECharts option={qualityRatingOption} style={{ height: 380 }} /> <ReactECharts option={qualityRatingOption} style={{ height: 380 }} />
......
...@@ -15,10 +15,11 @@ import { ...@@ -15,10 +15,11 @@ import {
TableRow, TableRow,
} from '@/catalyst-ui-kit/javascript/table'; } from '@/catalyst-ui-kit/javascript/table';
import TailwindEditableField from '@/pages/RecordDetailDrawer/components/TailwindEditableField'; import TailwindEditableField from '@/pages/RecordDetailDrawer/components/TailwindEditableField';
import { updateRecord } from '@/services/DataService'; import { loadRecord, updateRecord } from '@/services/DataService';
import { normalizeText } from '@/utils/subjectView'; import { normalizeText } from '@/utils/subjectView';
import clsx from 'clsx'; import clsx from 'clsx';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LockClosedIcon } from '@heroicons/react/24/outline';
import { useContext, useEffect, useMemo, useState } from 'react'; import { useContext, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'umi'; import { useSearchParams } from 'umi';
import { UserContext } from '@/wrapper/Auth'; import { UserContext } from '@/wrapper/Auth';
...@@ -180,6 +181,7 @@ export default function ProblemRectificationFormPage() { ...@@ -180,6 +181,7 @@ export default function ProblemRectificationFormPage() {
const currentStep = flowData?.currentStep || 'editor'; const currentStep = flowData?.currentStep || 'editor';
const viewerRole = flowData?.viewerRole || 'guest'; const viewerRole = flowData?.viewerRole || 'guest';
const canEditRecords = Boolean(flowData?.canEditRecords); const canEditRecords = Boolean(flowData?.canEditRecords);
const hasLockedRecords = relatedRecords.some((record) => record.isLocked);
const metadata = messageRecord?.metadata || {}; const metadata = messageRecord?.metadata || {};
const fieldMap = useMemo(() => { const fieldMap = useMemo(() => {
...@@ -227,6 +229,13 @@ export default function ProblemRectificationFormPage() { ...@@ -227,6 +229,13 @@ export default function ProblemRectificationFormPage() {
return; return;
} }
const latestRecord = await loadRecord(selectedRecord.id);
if (selectedRecord.isLocked || latestRecord?.metadata?.is_locked === true) {
setError('当前问题整改记录已锁定,不允许修改。');
await refreshAfterMutation();
return;
}
await updateRecord(selectedRecord.id, { await updateRecord(selectedRecord.id, {
metadata: { metadata: {
[fieldName]: [fieldName]:
...@@ -246,6 +255,11 @@ export default function ProblemRectificationFormPage() { ...@@ -246,6 +255,11 @@ export default function ProblemRectificationFormPage() {
return; return;
} }
if (hasLockedRecords) {
setError('当前流程包含已锁定的问题整改记录,不允许继续修改或审批。');
return;
}
setSubmitting(true); setSubmitting(true);
setError(''); setError('');
...@@ -307,8 +321,9 @@ export default function ProblemRectificationFormPage() { ...@@ -307,8 +321,9 @@ export default function ProblemRectificationFormPage() {
}); });
}; };
const actionHint = const actionHint = hasLockedRecords
viewerRole === 'editor' ? '当前流程包含已锁定记录,仅可查看;解锁后方可继续处理。'
: viewerRole === 'editor'
? '你当前是填写人,请完善整改内容后点击送审。' ? '你当前是填写人,请完善整改内容后点击送审。'
: viewerRole === 'auditor' : viewerRole === 'auditor'
? '你当前是复核人,请核对整改内容后进行通过或驳回。' ? '你当前是复核人,请核对整改内容后进行通过或驳回。'
...@@ -418,7 +433,15 @@ export default function ProblemRectificationFormPage() { ...@@ -418,7 +433,15 @@ export default function ProblemRectificationFormPage() {
{relatedRecords.map((record) => ( {relatedRecords.map((record) => (
<TableRow key={record.id}> <TableRow key={record.id}>
<TableCell> <TableCell>
<div className="font-medium text-slate-900">{record.title || '-'}</div> <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> <div className="mt-1 text-xs text-slate-500">{record.projectName || '-'}</div>
</TableCell> </TableCell>
<TableCell>{record.feedbackUnit || '-'}</TableCell> <TableCell>{record.feedbackUnit || '-'}</TableCell>
...@@ -437,7 +460,7 @@ export default function ProblemRectificationFormPage() { ...@@ -437,7 +460,7 @@ export default function ProblemRectificationFormPage() {
outline outline
onClick={() => setSelectedRecord(record)} onClick={() => setSelectedRecord(record)}
> >
{canEditRecords ? '查看/填写' : '查看详情'} {canEditRecords && !record.isLocked ? '查看/填写' : '查看详情'}
</Button> </Button>
</TableCell> </TableCell>
</TableRow> </TableRow>
...@@ -465,17 +488,17 @@ export default function ProblemRectificationFormPage() { ...@@ -465,17 +488,17 @@ export default function ProblemRectificationFormPage() {
</div> </div>
<div className="mt-5 flex flex-wrap gap-3"> <div className="mt-5 flex flex-wrap gap-3">
{currentStep === 'editor' && viewerRole === 'editor' ? ( {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 ? '送审中...' : '填写完成并送审'} {submitting ? '送审中...' : '填写完成并送审'}
</Button> </Button>
) : null} ) : null}
{currentStep === 'auditor' && viewerRole === 'auditor' ? ( {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 ? '处理中...' : '复核通过'} {submitting ? '处理中...' : '复核通过'}
</Button> </Button>
<Button color="red" disabled={submitting || relatedRecords.length === 0} onClick={handleAuditorReject}> <Button color="red" disabled={submitting || hasLockedRecords || relatedRecords.length === 0} onClick={handleAuditorReject}>
{submitting ? '处理中...' : '驳回复填'} {submitting ? '处理中...' : '驳回复填'}
</Button> </Button>
</> </>
...@@ -483,10 +506,10 @@ export default function ProblemRectificationFormPage() { ...@@ -483,10 +506,10 @@ export default function ProblemRectificationFormPage() {
{currentStep === 'sender' && viewerRole === 'sender' ? ( {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 ? '处理中...' : '最终审核通过'} {submitting ? '处理中...' : '最终审核通过'}
</Button> </Button>
<Button color="red" disabled={submitting || relatedRecords.length === 0} onClick={handleSenderReject}> <Button color="red" disabled={submitting || hasLockedRecords || relatedRecords.length === 0} onClick={handleSenderReject}>
{submitting ? '处理中...' : '最终审核驳回'} {submitting ? '处理中...' : '最终审核驳回'}
</Button> </Button>
</> </>
...@@ -512,6 +535,11 @@ export default function ProblemRectificationFormPage() { ...@@ -512,6 +535,11 @@ export default function ProblemRectificationFormPage() {
<DialogBody> <DialogBody>
{selectedRecord ? ( {selectedRecord ? (
<div className="space-y-4"> <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"> <div className="grid gap-4 md:grid-cols-2">
<SummaryCard label="问题标题" value={selectedRecord.title || '-'} /> <SummaryCard label="问题标题" value={selectedRecord.title || '-'} />
<SummaryCard label="反馈单位" value={selectedRecord.feedbackUnit || '-'} /> <SummaryCard label="反馈单位" value={selectedRecord.feedbackUnit || '-'} />
...@@ -526,8 +554,8 @@ export default function ProblemRectificationFormPage() { ...@@ -526,8 +554,8 @@ export default function ProblemRectificationFormPage() {
label={fieldDef.label || fieldDef.name} label={fieldDef.label || fieldDef.name}
fieldDef={fieldDef} fieldDef={fieldDef}
value={selectedRecord.raw?.metadata?.[fieldDef.name]} value={selectedRecord.raw?.metadata?.[fieldDef.name]}
readonly={!canEditRecords} readonly={!canEditRecords || selectedRecord.isLocked}
disabled={!canEditRecords} disabled={!canEditRecords || selectedRecord.isLocked}
onSave={(nextValue) => handleSaveField(fieldDef.name, nextValue)} onSave={(nextValue) => handleSaveField(fieldDef.name, nextValue)}
/> />
))} ))}
......
...@@ -150,34 +150,29 @@ function normalizeRatingLevel(value) { ...@@ -150,34 +150,29 @@ function normalizeRatingLevel(value) {
return RATING_LEVELS.includes(text) ? text : ''; return RATING_LEVELS.includes(text) ? text : '';
} }
function calculateQualityRating(record) { export function calculateRectificationQualityRating(record) {
const explicitRating = normalizeRatingLevel(record.overallRating);
if (explicitRating) {
return explicitRating;
}
const dimensions = [ const dimensions = [
normalizeRatingLevel(record.timelinessLevel), normalizeRatingLevel(record.timelinessLevel),
normalizeRatingLevel(record.materialQualityLevel), normalizeRatingLevel(record.materialQualityLevel),
record.involvesPolicyRevision ? '高' : '', record.involvesPolicyRevision ? '高' : '',
record.involvesSystemConstruction ? '高' : '', record.involvesSystemConstruction ? '高' : '',
record.isHistoricalProblem ? '低' : '', record.isHistoricalProblem ? '高' : '',
record.isRepeatedProblem ? '低' : '', record.isRepeatedProblem ? '高' : '',
].filter(Boolean); ].filter(Boolean);
if (dimensions.length === 0) { if (dimensions.length === 0) {
return ''; return '';
} }
if (dimensions.includes('低')) {
return '低';
}
const highCount = dimensions.filter((item) => item === '高').length; const highCount = dimensions.filter((item) => item === '高').length;
if (highCount >= 2) { if (highCount >= 1) {
return '高'; return '高';
} }
if (dimensions.includes('低')) {
return '低';
}
return '中'; return '中';
} }
...@@ -468,7 +463,6 @@ function normalizeProblemRecord(record) { ...@@ -468,7 +463,6 @@ function normalizeProblemRecord(record) {
rootCause: normalizeText(metadata.problem_root_cause), rootCause: normalizeText(metadata.problem_root_cause),
timelinessLevel: normalizeText(metadata.rectification_timeliness_level), timelinessLevel: normalizeText(metadata.rectification_timeliness_level),
materialQualityLevel: normalizeText(metadata.rectification_material_quality_level), materialQualityLevel: normalizeText(metadata.rectification_material_quality_level),
overallRating: normalizeText(metadata.rectification_overall_rating),
primaryResponsibleDepartment: extractPrimaryDepartment(metadata), primaryResponsibleDepartment: extractPrimaryDepartment(metadata),
responsibleDepartments: toArray(metadata.responsible_department) responsibleDepartments: toArray(metadata.responsible_department)
.map((item) => normalizeText(item)) .map((item) => normalizeText(item))
...@@ -500,7 +494,7 @@ function normalizeProblemRecord(record) { ...@@ -500,7 +494,7 @@ function normalizeProblemRecord(record) {
return { return {
...normalizedRecord, ...normalizedRecord,
qualityRating: calculateQualityRating(normalizedRecord), qualityRating: calculateRectificationQualityRating(normalizedRecord),
}; };
} }
......
...@@ -153,6 +153,29 @@ function buildReturnUrl(messageName) { ...@@ -153,6 +153,29 @@ function buildReturnUrl(messageName) {
return getProblemRectificationFormUrl(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) { function resolveCurrentStep(messageRecord) {
const metadata = messageRecord?.metadata || {}; const metadata = messageRecord?.metadata || {};
const editorStatus = metadata.editor_status || DEFAULT_STATUS; const editorStatus = metadata.editor_status || DEFAULT_STATUS;
...@@ -280,6 +303,7 @@ export async function markEditorComplete({ ...@@ -280,6 +303,7 @@ export async function markEditorComplete({
currentUser, currentUser,
auditors, auditors,
}) { }) {
await assertRelatedRecordsUnlocked(messageRecord);
const metadata = messageRecord?.metadata || {}; const metadata = messageRecord?.metadata || {};
const messageName = messageRecord?.name || messageRecord?._id; const messageName = messageRecord?.name || messageRecord?._id;
const returnUrl = buildReturnUrl(messageName); const returnUrl = buildReturnUrl(messageName);
...@@ -324,6 +348,7 @@ export async function approveByAuditor({ ...@@ -324,6 +348,7 @@ export async function approveByAuditor({
currentUser, currentUser,
sender, sender,
}) { }) {
await assertRelatedRecordsUnlocked(messageRecord);
const metadata = messageRecord?.metadata || {}; const metadata = messageRecord?.metadata || {};
const messageName = messageRecord?.name || messageRecord?._id; const messageName = messageRecord?.name || messageRecord?._id;
const returnUrl = buildReturnUrl(messageName); const returnUrl = buildReturnUrl(messageName);
...@@ -368,6 +393,7 @@ export async function rejectByAuditor({ ...@@ -368,6 +393,7 @@ export async function rejectByAuditor({
currentUser, currentUser,
editors, editors,
}) { }) {
await assertRelatedRecordsUnlocked(messageRecord);
const metadata = messageRecord?.metadata || {}; const metadata = messageRecord?.metadata || {};
const messageName = messageRecord?.name || messageRecord?._id; const messageName = messageRecord?.name || messageRecord?._id;
const returnUrl = buildReturnUrl(messageName); const returnUrl = buildReturnUrl(messageName);
...@@ -411,6 +437,7 @@ export async function rejectByAuditor({ ...@@ -411,6 +437,7 @@ export async function rejectByAuditor({
export async function approveBySender({ export async function approveBySender({
messageRecord, messageRecord,
}) { }) {
await assertRelatedRecordsUnlocked(messageRecord);
const metadata = messageRecord?.metadata || {}; const metadata = messageRecord?.metadata || {};
await batchUpdateRecords({ await batchUpdateRecords({
...@@ -437,6 +464,7 @@ export async function rejectBySender({ ...@@ -437,6 +464,7 @@ export async function rejectBySender({
currentUser, currentUser,
editors, editors,
}) { }) {
await assertRelatedRecordsUnlocked(messageRecord);
const metadata = messageRecord?.metadata || {}; const metadata = messageRecord?.metadata || {};
const messageName = messageRecord?.name || messageRecord?._id; const messageName = messageRecord?.name || messageRecord?._id;
const returnUrl = buildReturnUrl(messageName); const returnUrl = buildReturnUrl(messageName);
......
...@@ -103,7 +103,7 @@ type Record = { ...@@ -103,7 +103,7 @@ type Record = {
一条记录表示一条待整改问题主记录,是本模块最核心的数据对象。 一条记录表示一条待整改问题主记录,是本模块最核心的数据对象。
当前字段定义来自线上 Subject 配置,共 43 个字段。字段值写入时均放入 `metadata`。 当前字段定义来自线上 Subject 配置。字段值写入时均放入 `metadata`。
| 字段标签 | 字段别名 | 字段类型 | 默认值 | 是否主键 | 是否多值 | 是否必填 | 可搜索 | 可聚合 | | 字段标签 | 字段别名 | 字段类型 | 默认值 | 是否主键 | 是否多值 | 是否必填 | 可搜索 | 可聚合 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- | --- | --- |
...@@ -151,6 +151,8 @@ type Record = { ...@@ -151,6 +151,8 @@ type Record = {
| 实际检查完成日期 | `inspection_completed_at` | text | `null` | 否 | 否 | 否 | 否 | 否 | | 实际检查完成日期 | `inspection_completed_at` | text | `null` | 否 | 否 | 否 | 否 | 否 |
| 主责部门 | `primary_responsible_department` | text | `null` | 否 | 否 | 否 | 否 | 否 | | 主责部门 | `primary_responsible_department` | text | `null` | 否 | 否 | 否 | 否 | 否 |
| 部门整改明细 | `department_rectification_items` | object | `[]` | 否 | 是 | 否 | 否 | 否 | | 部门整改明细 | `department_rectification_items` | object | `[]` | 否 | 是 | 否 | 否 | 否 |
| 延期申请 | `problem_rectification_delay_request` | subject | `null` | 否 | 否 | 否 | 否 | 否 |
| 状态锁定 | `is_locked` | boolean | `false` | 否 | 否 | 否 | 否 | 否 |
### 3.1.1 select 字段选项 ### 3.1.1 select 字段选项
...@@ -175,6 +177,8 @@ type Record = { ...@@ -175,6 +177,8 @@ type Record = {
- 其余 `select` 类型的可选值必须以线上 Subject 配置中的 `settings.options` 为准,禁止在页面内硬编码 - 其余 `select` 类型的可选值必须以线上 Subject 配置中的 `settings.options` 为准,禁止在页面内硬编码
- `problem_discovery_situation` 曾在历史文档和归档对象中出现,但本次主对象字段定义未包含;新页面只应在 Subject View 或线上字段配置存在时展示,不能作为 `problem_rectification` 固定字段依赖 - `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` 以明细为事实来源,父字段仅做兼容汇总展示 - `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 等流程控制字段不作为普通表单字段开放 - 部门明细的填写人只能编辑自己负责明细的整改业务字段,送审后(`editor_status = "已完成"`)不再允许编辑;审批人审批通过后(`audit_status = "已审批"`)不再允许编辑,审批驳回后(`audit_status = "已驳回"`)审批人可以编辑该明细的整改业务字段,填写人也恢复编辑。审批人恢复的只是业务字段编辑权限,不获得填写人的送审权限。流程推送人可以编辑本次推送关联的父问题记录及全部部门明细整改业务字段。审批状态、待办 ID、明细 ID 等流程控制字段不作为普通表单字段开放
- 推送人修改父问题记录或部门明细时,不自动重置该明细已经形成的审批状态,也不自动重新生成流程待办 - 推送人修改父问题记录或部门明细时,不自动重置该明细已经形成的审批状态,也不自动重新生成流程待办
...@@ -188,6 +192,7 @@ type Record = { ...@@ -188,6 +192,7 @@ type Record = {
| 审批人 | `auditor` | user | 是 | 保存用户 `_id`,每条明细 1 个审批人 | | 审批人 | `auditor` | user | 是 | 保存用户 `_id`,每条明细 1 个审批人 |
| 整改进度 | `rectification_progress` | select/text | 否 | 填写人维护,优先使用主对象整改进度选项 | | 整改进度 | `rectification_progress` | select/text | 否 | 填写人维护,优先使用主对象整改进度选项 |
| 整改情况 | `rectification_situation` | long_text | 否 | 填写人维护 | | 整改情况 | `rectification_situation` | long_text | 否 | 填写人维护 |
| 延期事由(兼容) | `delay_reason` | long_text | 否 | 已存在的明细字段;新延期流程改为写入问题主记录平级嵌套对象的同名字段,本字段仅保留兼容数据 |
| 预计整改完成时间 | `the_time_of_rectification` | date | 否 | 填写人维护,提交时保存 ISO 8601 | | 预计整改完成时间 | `the_time_of_rectification` | date | 否 | 填写人维护,提交时保存 ISO 8601 |
| 整改证明材料 | `rectification_proof_materials` | file | 否 | 填写人上传,按部门明细独立保存 | | 整改证明材料 | `rectification_proof_materials` | file | 否 | 填写人上传,按部门明细独立保存 |
| 填写状态 | `editor_status` | text | 否 | `未完成`、`已完成` | | 填写状态 | `editor_status` | text | 否 | `未完成`、`已完成` |
...@@ -199,7 +204,39 @@ type Record = { ...@@ -199,7 +204,39 @@ type Record = {
| 更新人 | `updated_by` | user | 否 | 最近更新用户 `_id` | | 更新人 | `updated_by` | user | 否 | 最近更新用户 `_id` |
| 更新时间 | `updated_at` | date | 否 | 最近更新时间 | | 更新时间 | `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 ```json
{ {
...@@ -326,7 +363,7 @@ type Record = { ...@@ -326,7 +363,7 @@ type Record = {
- `restriction_value` 保存被推送整改记录的 `_id[]` - `restriction_value` 保存被推送整改记录的 `_id[]`
- `reference_record_id` 与 `restriction_value` 保持一致,作为显式的“关联记录 ID”冗余字段 - `reference_record_id` 与 `restriction_value` 保持一致,作为显式的“关联记录 ID”冗余字段
- `subject_name` 保存被推送记录所属对象名,当前整改流程固定为 `problem_rectification` - `subject_name` 保存被推送记录所属对象名,当前整改流程固定为 `problem_rectification`
- `flow_mode` 区分新旧流程:`department` 表示单条问题按责任部门生成明细行,`batch` 表示多选问题的单填写人 / 单审批人简单推送;历史数据可能为空 - `flow_mode` 区分流程:`department` 表示单条问题按责任部门生成整改明细行,`batch` 表示多选问题的单填写人 / 单审批人简单推送,`delay` 表示问题级延期申请;历史 `delay_department` 值仅保留页面识别兼容
- `department_item_ids` 保存本次部门流程涉及的 `department_rectification_items[].item_id` - `department_item_ids` 保存本次部门流程涉及的 `department_rectification_items[].item_id`
- `progress_id_editor`、`progress_id_auditor`、`progress_id_sender` 用于记录每一步用户确认动作生成的流程 ID,用来区分当前流程推进到了哪一步 - `progress_id_editor`、`progress_id_auditor`、`progress_id_sender` 用于记录每一步用户确认动作生成的流程 ID,用来区分当前流程推进到了哪一步
- `editor_status`:当前项目统一使用 `未完成`、`已完成` - `editor_status`:当前项目统一使用 `未完成`、`已完成`
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment