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>
) : ( ) : (
<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 <SubjectViewContent
view={activeView} view={activeView}
subject={subject} subject={subject}
editableRecord={editableRecord} editableRecord={editableRecord}
onFieldUpdate={handleFieldUpdate} onFieldUpdate={handleFieldUpdate}
onDataChange={onDataChange} onDataChange={onDataChange}
readonly={readonly} readonly={effectiveReadonly}
isFieldDisabled={isDrawerFieldDisabled} 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,6 +810,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' } ...@@ -809,6 +810,7 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
{!loading && !dashboardData.isEmpty && !activeScope.isEmpty ? ( {!loading && !dashboardData.isEmpty && !activeScope.isEmpty ? (
<> <>
{!isBusinessDashboard ? ( {!isBusinessDashboard ? (
<>
<section className="space-y-5"> <section className="space-y-5">
<SectionHeading <SectionHeading
eyebrow="Overview" eyebrow="Overview"
...@@ -848,17 +850,13 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' } ...@@ -848,17 +850,13 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
/> />
</div> </div>
</section> </section>
) : null}
{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="按整改进度统计数量与占比,直观看当前积压主要落在哪个状态。"
...@@ -869,9 +867,12 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' } ...@@ -869,9 +867,12 @@ export function ProblemRectificationDashboardPage({ dashboardMode = 'overview' }
<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 }} />
......
...@@ -25,7 +25,7 @@ import { ...@@ -25,7 +25,7 @@ import {
import { normalizeText, toArray } from '@/utils/subjectView'; import { normalizeText, toArray } from '@/utils/subjectView';
import clsx from 'clsx'; import clsx from 'clsx';
import dayjs from 'dayjs'; 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 { useContext, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'umi'; import { useSearchParams } from 'umi';
import { UserContext } from '@/wrapper/Auth'; import { UserContext } from '@/wrapper/Auth';
...@@ -33,13 +33,17 @@ import { ...@@ -33,13 +33,17 @@ import {
AUDIT_STATUS_APPROVED, AUDIT_STATUS_APPROVED,
AUDIT_STATUS_PENDING, AUDIT_STATUS_PENDING,
AUDIT_STATUS_REJECTED, AUDIT_STATUS_REJECTED,
DELAY_REQUEST_FIELD,
EDITOR_STATUS_COMPLETED, EDITOR_STATUS_COMPLETED,
EDITOR_STATUS_PENDING, EDITOR_STATUS_PENDING,
FLOW_MODE_BATCH, FLOW_MODE_BATCH,
FLOW_MODE_DELAY,
FLOW_MODE_DELAY_DEPARTMENT,
FLOW_MODE_DEPARTMENT, FLOW_MODE_DEPARTMENT,
PROBLEM_RECTIFICATION_SUBJECT, PROBLEM_RECTIFICATION_SUBJECT,
buildDepartmentParentBatchUpdateData, buildDepartmentParentBatchUpdateData,
getProblemRectificationFlowTableUrl, getProblemRectificationFlowTableUrl,
normalizeDelayRequest,
normalizeDepartmentRectificationItems, normalizeDepartmentRectificationItems,
} from '../problemRectificationListService'; } from '../problemRectificationListService';
...@@ -49,6 +53,7 @@ const FLOW_MANAGED_PARENT_FIELDS = [ ...@@ -49,6 +53,7 @@ const FLOW_MANAGED_PARENT_FIELDS = [
'audit_status', 'audit_status',
'final_audit_status', 'final_audit_status',
'department_rectification_items', 'department_rectification_items',
DELAY_REQUEST_FIELD,
]; ];
const DEPARTMENT_MANAGED_PARENT_FIELDS = [ const DEPARTMENT_MANAGED_PARENT_FIELDS = [
...FLOW_MANAGED_PARENT_FIELDS, ...FLOW_MANAGED_PARENT_FIELDS,
...@@ -117,7 +122,7 @@ function collectUsers(ids = []) { ...@@ -117,7 +122,7 @@ function collectUsers(ids = []) {
return Array.from(new Set(ids.map((item) => toId(item)).filter(Boolean))); 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) { if (!recipientId) {
return ''; return '';
} }
...@@ -138,6 +143,9 @@ async function createProgressTask({ recipientId, title, currentUser, returnUrl } ...@@ -138,6 +143,9 @@ async function createProgressTask({ recipientId, title, currentUser, returnUrl }
return result?.data?.taskId || buildProgressFallback('progress'); return result?.data?.taskId || buildProgressFallback('progress');
} catch (error) { } catch (error) {
if (!fallbackOnError) {
throw error;
}
return buildProgressFallback('progress'); return buildProgressFallback('progress');
} }
} }
...@@ -200,6 +208,7 @@ function buildDepartmentRows(records = []) { ...@@ -200,6 +208,7 @@ function buildDepartmentRows(records = []) {
progressIdEditor: item.progress_id_editor || '', progressIdEditor: item.progress_id_editor || '',
progressIdAuditor: item.progress_id_auditor || '', progressIdAuditor: item.progress_id_auditor || '',
progressIdSender: item.progress_id_sender || '', progressIdSender: item.progress_id_sender || '',
isLocked: record.metadata?.is_locked === true,
})); }));
}); });
} }
...@@ -234,10 +243,51 @@ function buildBatchRows(records = [], metadata = {}) { ...@@ -234,10 +243,51 @@ function buildBatchRows(records = [], metadata = {}) {
progressIdEditor: toArray(metadata.progress_id_editor)[0] || '', progressIdEditor: toArray(metadata.progress_id_editor)[0] || '',
progressIdAuditor: toArray(metadata.progress_id_auditor)[0] || '', progressIdAuditor: toArray(metadata.progress_id_auditor)[0] || '',
progressIdSender: metadata.progress_id_sender || '', 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() { export default function ProblemRectificationFlowTablePage() {
const userInfo = useContext(UserContext); const userInfo = useContext(UserContext);
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
...@@ -254,11 +304,16 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -254,11 +304,16 @@ export default function ProblemRectificationFlowTablePage() {
const metadata = useMemo(() => messageRecord?.metadata || {}, [messageRecord]); const metadata = useMemo(() => messageRecord?.metadata || {}, [messageRecord]);
const flowMode = metadata.flow_mode || FLOW_MODE_BATCH; const flowMode = metadata.flow_mode || FLOW_MODE_BATCH;
const isDelayFlow = flowMode === FLOW_MODE_DELAY || flowMode === FLOW_MODE_DELAY_DEPARTMENT;
const rows = useMemo( 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) ? buildDepartmentRows(records)
: buildBatchRows(records, metadata), : buildBatchRows(records, metadata);
},
[flowMode, metadata, records], [flowMode, metadata, records],
); );
const currentUserId = toId(userInfo?._id); const currentUserId = toId(userInfo?._id);
...@@ -356,6 +411,8 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -356,6 +411,8 @@ export default function ProblemRectificationFlowTablePage() {
situation: row.situation, situation: row.situation,
dueDate: formatDateInput(row.dueDate), dueDate: formatDateInput(row.dueDate),
proof: row.proof, proof: row.proof,
requestedDueDate: formatDateInput(row.requestedDueDate),
delayReason: row.delayReason || '',
...(drafts[row.key] || {}), ...(drafts[row.key] || {}),
}); });
...@@ -390,6 +447,37 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -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) => { const updateBatchRow = async (row, patch) => {
await updateRecord(row.recordId, { await updateRecord(row.recordId, {
metadata: { metadata: {
...@@ -404,11 +492,20 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -404,11 +492,20 @@ export default function ProblemRectificationFlowTablePage() {
}; };
const withRowSubmit = async (row, handler) => { const withRowSubmit = async (row, handler) => {
if (row.isLocked) {
setError('当前问题整改记录已锁定,不允许继续修改或审批。');
return;
}
setSubmittingKey(row.key); setSubmittingKey(row.key);
setError(''); setError('');
setSuccess(''); setSuccess('');
try { try {
const latestRecord = await loadRecord(row.recordId);
if (latestRecord?.metadata?.is_locked === true) {
throw new Error('当前问题整改记录已锁定,不允许继续修改或审批。');
}
await handler(); await handler();
await loadFlow(); await loadFlow();
setSuccess('操作已完成。'); setSuccess('操作已完成。');
...@@ -468,6 +565,139 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -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) => { const handleEditorSubmit = async (row) => {
await withRowSubmit(row, async () => { await withRowSubmit(row, async () => {
const savedPatch = await saveRowDraft(row); const savedPatch = await saveRowDraft(row);
...@@ -619,20 +849,41 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -619,20 +849,41 @@ export default function ProblemRectificationFlowTablePage() {
}; };
const canEditRow = (row) => const canEditRow = (row) =>
!row.isLocked && (
isSender || isSender ||
(row.editor === currentUserId && row.editorStatus !== EDITOR_STATUS_COMPLETED) || (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) => const canSubmitRow = (row) =>
!row.isLocked &&
row.editor === currentUserId && row.editor === currentUserId &&
row.editorStatus !== EDITOR_STATUS_COMPLETED; row.editorStatus !== EDITOR_STATUS_COMPLETED;
const canAuditRow = (row) => const canAuditRow = (row) =>
!row.isLocked &&
row.auditor === currentUserId && row.auditor === currentUserId &&
row.editorStatus === EDITOR_STATUS_COMPLETED && row.editorStatus === EDITOR_STATUS_COMPLETED &&
row.auditStatus !== AUDIT_STATUS_APPROVED; row.auditStatus !== AUDIT_STATUS_APPROVED;
const canFinalAuditRow = (row) => const canFinalAuditRow = (row) =>
!row.isLocked &&
senderId === currentUserId && senderId === currentUserId &&
row.auditStatus === AUDIT_STATUS_APPROVED && row.auditStatus === AUDIT_STATUS_APPROVED &&
row.finalAuditStatus !== 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 ( return (
<div className="space-y-6 p-6"> <div className="space-y-6 p-6">
...@@ -642,9 +893,12 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -642,9 +893,12 @@ export default function ProblemRectificationFlowTablePage() {
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-rose-600"> <div className="text-xs font-semibold uppercase tracking-[0.24em] text-rose-600">
Problem Rectification Flow Problem Rectification Flow
</div> </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"> <p className="mt-2 text-sm text-slate-500">
当前流程:{messageId || '-'};流程模式:{flowMode === FLOW_MODE_DEPARTMENT ? '按责任部门' : '批量简单推送'} 当前流程:{messageId || '-'};流程模式:
{isDelayFlow ? '问题延期申请' : flowMode === FLOW_MODE_DEPARTMENT ? '按责任部门' : '批量简单推送'}
</p> </p>
</div> </div>
<Button outline onClick={loadFlow} disabled={loading || Boolean(submittingKey)}> <Button outline onClick={loadFlow} disabled={loading || Boolean(submittingKey)}>
...@@ -664,7 +918,9 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -664,7 +918,9 @@ export default function ProblemRectificationFlowTablePage() {
{loading ? ( {loading ? (
<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">正在加载流程表格...</div>
) : rows.length === 0 ? ( ) : 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)]"> <Table bleed dense className="[--gutter:--spacing(4)]">
<TableHead> <TableHead>
...@@ -674,10 +930,20 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -674,10 +930,20 @@ export default function ProblemRectificationFlowTablePage() {
<TableHeader>涉及业务领域</TableHeader> <TableHeader>涉及业务领域</TableHeader>
<TableHeader>责任部门</TableHeader> <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>
<TableHeader>证明材料</TableHeader> <TableHeader>证明材料</TableHeader>
</>
)}
<TableHeader>状态</TableHeader> <TableHeader>状态</TableHeader>
<TableHeader>操作</TableHeader> <TableHeader>操作</TableHeader>
</TableRow> </TableRow>
...@@ -685,13 +951,21 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -685,13 +951,21 @@ export default function ProblemRectificationFlowTablePage() {
<TableBody> <TableBody>
{rows.map((row) => { {rows.map((row) => {
const draft = getDraft(row); const draft = getDraft(row);
const editable = canEditRow(row); const editable = isDelayFlow ? canEditDelayRow(row) : canEditRow(row);
const submitting = submittingKey === row.key; const submitting = submittingKey === row.key;
return ( return (
<TableRow key={row.key} className="align-top"> <TableRow key={row.key} className="align-top">
<TableCell className="min-w-64"> <TableCell className="min-w-64">
<div className="flex flex-wrap items-center gap-2">
<div className="font-medium text-slate-900">{row.title}</div> <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> <div className="mt-1 text-xs text-slate-500">{row.projectName}</div>
{isSender ? ( {isSender ? (
<Button <Button
...@@ -701,7 +975,7 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -701,7 +975,7 @@ export default function ProblemRectificationFlowTablePage() {
onClick={() => setEditingRecordName(row.record?.name || row.recordId)} onClick={() => setEditingRecordName(row.record?.name || row.recordId)}
> >
<PencilSquareIcon data-slot="icon" /> <PencilSquareIcon data-slot="icon" />
编辑问题记录 {row.isLocked ? '查看问题记录' : '编辑问题记录'}
</Button> </Button>
) : null} ) : null}
</TableCell> </TableCell>
...@@ -720,6 +994,42 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -720,6 +994,42 @@ export default function ProblemRectificationFlowTablePage() {
<div className="text-xs text-slate-500">填:{getUserLabel(row.editor)}</div> <div className="text-xs text-slate-500">填:{getUserLabel(row.editor)}</div>
<div className="mt-1 text-xs text-slate-500">审:{getUserLabel(row.auditor)}</div> <div className="mt-1 text-xs text-slate-500">审:{getUserLabel(row.auditor)}</div>
</TableCell> </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"> <TableCell className="min-w-36">
{editable ? ( {editable ? (
<Select <Select
...@@ -776,15 +1086,64 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -776,15 +1086,64 @@ export default function ProblemRectificationFlowTablePage() {
/> />
) : null} ) : null}
</TableCell> </TableCell>
</>
)}
<TableCell className="min-w-44"> <TableCell className="min-w-44">
<div className="space-y-1.5"> <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} /> <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} /> <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} /> <StatusPill value={row.finalAuditStatus} />
</div> </div>
</div>
</TableCell> </TableCell>
<TableCell className="min-w-52"> <TableCell className="min-w-52">
<div className="flex flex-wrap gap-2"> <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 ? ( {editable ? (
<Button outline disabled={submitting} onClick={() => handleSave(row)}> <Button outline disabled={submitting} onClick={() => handleSave(row)}>
保存 保存
...@@ -818,6 +1177,8 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -818,6 +1177,8 @@ export default function ProblemRectificationFlowTablePage() {
{!editable && !canAuditRow(row) && !canFinalAuditRow(row) ? ( {!editable && !canAuditRow(row) && !canFinalAuditRow(row) ? (
<span className="text-xs text-slate-400">当前无可执行操作</span> <span className="text-xs text-slate-400">当前无可执行操作</span>
) : null} ) : null}
</>
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
...@@ -834,7 +1195,7 @@ export default function ProblemRectificationFlowTablePage() { ...@@ -834,7 +1195,7 @@ export default function ProblemRectificationFlowTablePage() {
subjectName={PROBLEM_RECTIFICATION_SUBJECT} subjectName={PROBLEM_RECTIFICATION_SUBJECT}
readonly={!isSender} readonly={!isSender}
disabledFields={ disabledFields={
flowMode === FLOW_MODE_DEPARTMENT flowMode === FLOW_MODE_DEPARTMENT || isDelayFlow
? DEPARTMENT_MANAGED_PARENT_FIELDS ? DEPARTMENT_MANAGED_PARENT_FIELDS
: FLOW_MANAGED_PARENT_FIELDS : FLOW_MANAGED_PARENT_FIELDS
} }
......
...@@ -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="flex flex-wrap items-center gap-2">
<div className="font-medium text-slate-900">{record.title || '-'}</div> <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)}
/> />
))} ))}
......
...@@ -34,6 +34,7 @@ import { ...@@ -34,6 +34,7 @@ import {
} from '@/services/DataService'; } from '@/services/DataService';
import * as Headless from '@headlessui/react'; import * as Headless from '@headlessui/react';
import { ChevronRightIcon } from '@heroicons/react/20/solid'; import { ChevronRightIcon } from '@heroicons/react/20/solid';
import { LockClosedIcon, LockOpenIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx'; import clsx from 'clsx';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
...@@ -48,9 +49,11 @@ import { ...@@ -48,9 +49,11 @@ import {
DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE, DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE,
PROBLEM_RECTIFICATION_SUBJECT, PROBLEM_RECTIFICATION_SUBJECT,
AUDIT_STATUS_PENDING, AUDIT_STATUS_PENDING,
DELAY_REQUEST_FIELD,
DEPARTMENT_RECTIFICATION_FIELD, DEPARTMENT_RECTIFICATION_FIELD,
EDITOR_STATUS_PENDING, EDITOR_STATUS_PENDING,
FLOW_MODE_BATCH, FLOW_MODE_BATCH,
FLOW_MODE_DELAY,
FLOW_MODE_DEPARTMENT, FLOW_MODE_DEPARTMENT,
PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS, PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS,
buildDepartmentAssignmentsFromRecord, buildDepartmentAssignmentsFromRecord,
...@@ -66,6 +69,7 @@ import { ...@@ -66,6 +69,7 @@ import {
loadProblemRectificationListData, loadProblemRectificationListData,
loadProblemRectificationListResources, loadProblemRectificationListResources,
loadProblemRectificationViewCountMap, loadProblemRectificationViewCountMap,
normalizeDelayRequest,
normalizeDepartmentRectificationItems, normalizeDepartmentRectificationItems,
} from '../problemRectificationListService'; } from '../problemRectificationListService';
...@@ -85,7 +89,6 @@ const FIELD_HINTS = { ...@@ -85,7 +89,6 @@ const FIELD_HINTS = {
problem_severity_level: '建议统一填写“高 / 中 / 低”', problem_severity_level: '建议统一填写“高 / 中 / 低”',
rectification_timeliness_level: '建议统一填写“高 / 中 / 低”', rectification_timeliness_level: '建议统一填写“高 / 中 / 低”',
rectification_material_quality_level: '建议统一填写“高 / 中 / 低”', rectification_material_quality_level: '建议统一填写“高 / 中 / 低”',
rectification_overall_rating: '建议统一填写“高 / 中 / 低”',
involves_policy_revision: '建议统一填写“是”或“否”', involves_policy_revision: '建议统一填写“是”或“否”',
involves_system_construction: '建议统一填写“是”或“否”', involves_system_construction: '建议统一填写“是”或“否”',
is_historical_problem: '建议统一填写“是”或“否”', is_historical_problem: '建议统一填写“是”或“否”',
...@@ -113,7 +116,6 @@ const CREATE_FIELD_OPTION_OVERRIDES = { ...@@ -113,7 +116,6 @@ const CREATE_FIELD_OPTION_OVERRIDES = {
involves_system_construction: ['是', '否'], involves_system_construction: ['是', '否'],
is_historical_problem: ['是', '否'], is_historical_problem: ['是', '否'],
is_repeated_problem: ['是', '否'], is_repeated_problem: ['是', '否'],
rectification_overall_rating: ['高', '中', '低'],
}; };
function StatusPill({ text, tone = 'default' }) { function StatusPill({ text, tone = 'default' }) {
...@@ -491,7 +493,7 @@ function FieldValuePreview({ column, record }) { ...@@ -491,7 +493,7 @@ function FieldValuePreview({ column, record }) {
} }
if (fieldName === 'rectification_overall_rating') { if (fieldName === 'rectification_overall_rating') {
const text = formatFieldValue(value, column.fieldDef) || '未填写'; const text = record.overallRating || '待计算';
return <StatusPill text={text} tone={resolveLevelTone(text, true)} />; return <StatusPill text={text} tone={resolveLevelTone(text, true)} />;
} }
...@@ -576,6 +578,7 @@ export default function ProblemRectificationListPage() { ...@@ -576,6 +578,7 @@ export default function ProblemRectificationListPage() {
const [actionLoading, setActionLoading] = useState(false); const [actionLoading, setActionLoading] = useState(false);
const [filters, setFilters] = useState({ const [filters, setFilters] = useState({
keyword: '', keyword: '',
inspectionReportMonth: '',
progress: '', progress: '',
auditStatus: '', auditStatus: '',
severityLevel: '', severityLevel: '',
...@@ -589,11 +592,14 @@ export default function ProblemRectificationListPage() { ...@@ -589,11 +592,14 @@ export default function ProblemRectificationListPage() {
const [selectedIds, setSelectedIds] = useState([]); const [selectedIds, setSelectedIds] = useState([]);
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [showPushModal, setShowPushModal] = useState(false); const [showPushModal, setShowPushModal] = useState(false);
const [showDelayPushModal, setShowDelayPushModal] = useState(false);
const [cloneTargetRecord, setCloneTargetRecord] = useState(null); const [cloneTargetRecord, setCloneTargetRecord] = useState(null);
const [formValues, setFormValues] = useState({}); const [formValues, setFormValues] = useState({});
const [fillerUsers, setFillerUsers] = useState([]); const [fillerUsers, setFillerUsers] = useState([]);
const [auditorUsers, setAuditorUsers] = useState([]); const [auditorUsers, setAuditorUsers] = useState([]);
const [departmentAssignments, setDepartmentAssignments] = useState([]); const [departmentAssignments, setDepartmentAssignments] = useState([]);
const [delayEditorUsers, setDelayEditorUsers] = useState([]);
const [delayAuditorUsers, setDelayAuditorUsers] = useState([]);
const [successMessage, setSuccessMessage] = useState(''); const [successMessage, setSuccessMessage] = useState('');
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE); const [pageSize, setPageSize] = useState(DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE);
...@@ -785,6 +791,7 @@ export default function ProblemRectificationListPage() { ...@@ -785,6 +791,7 @@ export default function ProblemRectificationListPage() {
() => filteredRecords.filter((item) => selectedIds.includes(item.id)), () => filteredRecords.filter((item) => selectedIds.includes(item.id)),
[filteredRecords, selectedIds], [filteredRecords, selectedIds],
); );
const hasLockedSelection = selectedRecords.some((record) => record.isLocked);
useEffect(() => { useEffect(() => {
const visibleIds = new Set(filteredRecords.map((item) => item.id)); const visibleIds = new Set(filteredRecords.map((item) => item.id));
...@@ -847,6 +854,11 @@ export default function ProblemRectificationListPage() { ...@@ -847,6 +854,11 @@ export default function ProblemRectificationListPage() {
return; return;
} }
if (targetRecords.some((record) => record.isLocked)) {
setError('已锁定的问题整改记录不允许删除,请先解锁。');
return;
}
const confirmed = window.confirm(`确认删除 ${targetRecords.length} 条问题整改记录吗?此操作不可撤销。`); const confirmed = window.confirm(`确认删除 ${targetRecords.length} 条问题整改记录吗?此操作不可撤销。`);
if (!confirmed) { if (!confirmed) {
return; return;
...@@ -882,6 +894,12 @@ export default function ProblemRectificationListPage() { ...@@ -882,6 +894,12 @@ export default function ProblemRectificationListPage() {
return; return;
} }
if (cloneTargetRecord.isLocked) {
setError('已锁定的问题整改记录不允许复制,请先解锁。');
setCloneTargetRecord(null);
return;
}
const recordName = cloneTargetRecord.name || cloneTargetRecord.id; const recordName = cloneTargetRecord.name || cloneTargetRecord.id;
if (!recordName) { if (!recordName) {
setError('当前记录缺少可复制的 name 标识。'); setError('当前记录缺少可复制的 name 标识。');
...@@ -906,6 +924,41 @@ export default function ProblemRectificationListPage() { ...@@ -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 handleSearchUsers = useCallback(async (keyword) => {
const filter = keyword.trim() const filter = keyword.trim()
? { ? {
...@@ -933,6 +986,159 @@ export default function ProblemRectificationListPage() { ...@@ -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 handlePush = async () => {
const fillerIds = fillerUsers.map((user) => user._id).filter(Boolean); const fillerIds = fillerUsers.map((user) => user._id).filter(Boolean);
const auditorIds = auditorUsers.map((user) => user._id).filter(Boolean); const auditorIds = auditorUsers.map((user) => user._id).filter(Boolean);
...@@ -943,6 +1149,11 @@ export default function ProblemRectificationListPage() { ...@@ -943,6 +1149,11 @@ export default function ProblemRectificationListPage() {
return; return;
} }
if (selectedRecords.some((record) => record.isLocked)) {
setError('已锁定的问题整改记录不允许推送,请先解锁。');
return;
}
if (isDepartmentFlow) { if (isDepartmentFlow) {
const hasMissingDepartmentUser = departmentAssignments.some( const hasMissingDepartmentUser = departmentAssignments.some(
(item) => !item.editor?._id || !item.auditor?._id, (item) => !item.editor?._id || !item.auditor?._id,
...@@ -1122,6 +1333,11 @@ export default function ProblemRectificationListPage() { ...@@ -1122,6 +1333,11 @@ export default function ProblemRectificationListPage() {
type: 'text', type: 'text',
placeholder: '搜索项目名称、严重程度、分类、责任部门、问题原因...', placeholder: '搜索项目名称、严重程度、分类、责任部门、问题原因...',
}, },
{
key: 'inspectionReportMonth',
label: '检查报告出具月份',
type: 'month',
},
{ {
key: 'progress', key: 'progress',
label: '整改进度', label: '整改进度',
...@@ -1191,6 +1407,7 @@ export default function ProblemRectificationListPage() { ...@@ -1191,6 +1407,7 @@ export default function ProblemRectificationListPage() {
setPage(0); setPage(0);
setFilters({ setFilters({
keyword: '', keyword: '',
inspectionReportMonth: '',
progress: '', progress: '',
auditStatus: '', auditStatus: '',
severityLevel: '', severityLevel: '',
...@@ -1285,9 +1502,16 @@ export default function ProblemRectificationListPage() { ...@@ -1285,9 +1502,16 @@ export default function ProblemRectificationListPage() {
key: 'push', key: 'push',
label: '推送整改表', label: '推送整改表',
variant: 'outline', variant: 'outline',
disabled: () => selectedIds.length === 0 || actionLoading, disabled: () => selectedIds.length === 0 || hasLockedSelection || actionLoading,
onClick: () => setShowPushModal(true), onClick: () => setShowPushModal(true),
}, },
{
key: 'push-delay-request',
label: '推送延期申请',
variant: 'outline',
disabled: () => selectedIds.length !== 1 || hasLockedSelection || actionLoading,
onClick: openDelayPushModal,
},
{ {
key: 'export', key: 'export',
label: '导出', label: '导出',
...@@ -1307,7 +1531,7 @@ export default function ProblemRectificationListPage() { ...@@ -1307,7 +1531,7 @@ export default function ProblemRectificationListPage() {
label: '删除选中', label: '删除选中',
variant: 'solid', variant: 'solid',
tone: 'danger', tone: 'danger',
disabled: () => selectedIds.length === 0 || actionLoading, disabled: () => selectedIds.length === 0 || hasLockedSelection || actionLoading,
onClick: () => handleDelete(selectedRecords), onClick: () => handleDelete(selectedRecords),
}, },
]} ]}
...@@ -1318,11 +1542,29 @@ export default function ProblemRectificationListPage() { ...@@ -1318,11 +1542,29 @@ export default function ProblemRectificationListPage() {
variant: 'plain', variant: 'plain',
onClick: ({ openDetail, record }) => openDetail(record), 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', key: 'clone',
label: '复制', label: '复制',
variant: 'outline', variant: 'outline',
disabled: () => actionLoading, disabled: ({ record }) => actionLoading || record.isLocked,
onClick: ({ record }) => setCloneTargetRecord(record), onClick: ({ record }) => setCloneTargetRecord(record),
}, },
{ {
...@@ -1330,6 +1572,7 @@ export default function ProblemRectificationListPage() { ...@@ -1330,6 +1572,7 @@ export default function ProblemRectificationListPage() {
label: '删除', label: '删除',
variant: 'outline', variant: 'outline',
tone: 'danger', tone: 'danger',
disabled: ({ record }) => actionLoading || record.isLocked,
onClick: ({ record }) => handleDelete([record]), onClick: ({ record }) => handleDelete([record]),
}, },
]} ]}
...@@ -1342,6 +1585,7 @@ export default function ProblemRectificationListPage() { ...@@ -1342,6 +1585,7 @@ export default function ProblemRectificationListPage() {
recordName={recordName} recordName={recordName}
onClose={onClose} onClose={onClose}
onDataChange={onDataChange} onDataChange={onDataChange}
disabledFields={[DELAY_REQUEST_FIELD]}
preferredViewName={activeGridView?.title || activeGridView?.name} preferredViewName={activeGridView?.title || activeGridView?.name}
/> />
), ),
...@@ -1353,7 +1597,7 @@ export default function ProblemRectificationListPage() { ...@@ -1353,7 +1597,7 @@ export default function ProblemRectificationListPage() {
loadingTitle: '正在加载问题整改列表', loadingTitle: '正在加载问题整改列表',
loadingDescription: '正在读取对象结构、视图配置和记录数据,请稍候。', loadingDescription: '正在读取对象结构、视图配置和记录数据,请稍候。',
emptyTitle: '当前视图暂无数据', emptyTitle: '当前视图暂无数据',
emptyDescription: '可以切换其他视图,或调整严重程度、整改评级、主责部门和重点关注筛选。', emptyDescription: '可以切换其他视图,或调整检查报告出具月份、严重程度、整改评级、主责部门和重点关注筛选。',
}} }}
pagination={{ pagination={{
page, page,
...@@ -1376,13 +1620,17 @@ export default function ProblemRectificationListPage() { ...@@ -1376,13 +1620,17 @@ export default function ProblemRectificationListPage() {
const fieldPreview = <FieldValuePreview record={record} column={column} />; const fieldPreview = <FieldValuePreview record={record} column={column} />;
const fieldName = getFieldNameFromPath(column.field); const fieldName = getFieldNameFromPath(column.field);
if (fieldName !== 'title' || !expandable) { if (fieldName !== 'title') {
return fieldPreview; return fieldPreview;
} }
return ( return (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1"> <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
{fieldPreview} {fieldPreview}
{record.isLocked ? (
<StatusPill text="已锁定" tone="amber" />
) : null}
{expandable ? (
<Button <Button
plain plain
className="text-rose-700" className="text-rose-700"
...@@ -1395,6 +1643,7 @@ export default function ProblemRectificationListPage() { ...@@ -1395,6 +1643,7 @@ export default function ProblemRectificationListPage() {
/> />
{expanded ? '收起明细' : `${record.departmentItems.length} 条部门明细`} {expanded ? '收起明细' : `${record.departmentItems.length} 条部门明细`}
</Button> </Button>
) : null}
</div> </div>
); );
}} }}
...@@ -1574,6 +1823,61 @@ export default function ProblemRectificationListPage() { ...@@ -1574,6 +1823,61 @@ export default function ProblemRectificationListPage() {
</Dialog> </Dialog>
) : null} ) : 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 ? ( {cloneTargetRecord ? (
<Dialog open={Boolean(cloneTargetRecord)} onClose={() => setCloneTargetRecord(null)} size="lg"> <Dialog open={Boolean(cloneTargetRecord)} onClose={() => setCloneTargetRecord(null)} size="lg">
<DialogTitle>复制问题整改记录</DialogTitle> <DialogTitle>复制问题整改记录</DialogTitle>
......
...@@ -41,14 +41,14 @@ ...@@ -41,14 +41,14 @@
建议在当前项目中保留以下四个一级入口: 建议在当前项目中保留以下四个一级入口:
- 驾驶舱:`/integrity_risk_table/dashboard` - 驾驶舱:`/integrity_risk_table/dashboard`
- 业务驾驶舱:`/integrity_risk_table/business_dashboard` - 整改分析:`/integrity_risk_table/business_dashboard`
- 问题整改:`/problem_rectification` - 问题整改:`/problem_rectification`
- 往期材料归档:`/issue_rectification_archive` - 往期材料归档:`/issue_rectification_archive`
建议交互形态: 建议交互形态:
- 驾驶舱仅展示顶部横幅、部门分组切换与宏观总览 KPI 卡片 - 驾驶舱面向管理层,展示顶部横幅、部门分组切换、宏观总览 KPI 与整改进度分布
- 业务驾驶舱承接整改进度分布、问题风险定位、责任追踪与整改质效图表 - 整改分析承接问题风险定位、责任追踪与整改质效图表;“驾驶舱”命名仅用于管理层入口
- 问题整改主列表使用普通数据表格,不再依赖多维表格编辑器 - 问题整改主列表使用普通数据表格,不再依赖多维表格编辑器
- 新增、编辑、查看详情优先采用抽屉或弹窗,不走重型嵌套表格交互 - 新增、编辑、查看详情优先采用抽屉或弹窗,不走重型嵌套表格交互
- 批量操作放在顶部工具栏,不再依赖右键菜单作为唯一入口 - 批量操作放在顶部工具栏,不再依赖右键菜单作为唯一入口
...@@ -74,6 +74,7 @@ ...@@ -74,6 +74,7 @@
- `problem_rectification` 与 `issue_rectification_archive` 列表页统一复用 `RecordWorkbench` 作为“结构化表格 + 工具栏 + 详情抽屉入口”的工作台容器 - `problem_rectification` 与 `issue_rectification_archive` 列表页统一复用 `RecordWorkbench` 作为“结构化表格 + 工具栏 + 详情抽屉入口”的工作台容器
- 页面本身只负责提供 subject 对应的数据加载、统计口径、筛选 schema、行操作与批量操作 - 页面本身只负责提供 subject 对应的数据加载、统计口径、筛选 schema、行操作与批量操作
- `problem_rectification` 主列表采用传统分页表格,不一次性加载全部数据;每次按当前视图、筛选条件、关键词和页码调用 `/record/query`,分页参数使用 `page` 与 `page_size` - `problem_rectification` 主列表采用传统分页表格,不一次性加载全部数据;每次按当前视图、筛选条件、关键词和页码调用 `/record/query`,分页参数使用 `page` 与 `page_size`
- 主列表提供“检查报告出具月份”筛选,用户选择 `YYYY-MM` 后,仅查询 `metadata.inspection_report_date` 位于该月份的记录;该筛选不使用其它检查或整改日期兜底
- 主列表支持每页 `10 / 20 / 50 / 100` 条,切换视图、修改筛选或清空筛选时回到第 1 页;表头勾选只选择当前页记录,跨页批量处理需用户逐页确认选择范围 - 主列表支持每页 `10 / 20 / 50 / 100` 条,切换视图、修改筛选或清空筛选时回到第 1 页;表头勾选只选择当前页记录,跨页批量处理需用户逐页确认选择范围
- 主列表视图页签数量通过 `/record/count` 按各视图限制和当前筛选条件计算,表格标题区展示服务端筛选后的总数与当前页记录数 - 主列表视图页签数量通过 `/record/count` 按各视图限制和当前筛选条件计算,表格标题区展示服务端筛选后的总数与当前页记录数
- 主列表滚动到表格区域后,表格工作台吸顶展示;标题栏、操作区、列分组和分页固定在工作台内,数据表格区域独立滚动,表头列名在数据区域内继续吸顶,折叠 / 展开列分组时保留当前表格滚动位置 - 主列表滚动到表格区域后,表格工作台吸顶展示;标题栏、操作区、列分组和分页固定在工作台内,数据表格区域独立滚动,表头列名在数据区域内继续吸顶,折叠 / 展开列分组时保留当前表格滚动位置
...@@ -90,7 +91,7 @@ ...@@ -90,7 +91,7 @@
1. `integrity_risk_table/dashboard` 1. `integrity_risk_table/dashboard`
问题整改驾驶舱 问题整改驾驶舱
2. `integrity_risk_table/business_dashboard` 2. `integrity_risk_table/business_dashboard`
业务驾驶舱 问题整改分析
3. `problem_rectification` 3. `problem_rectification`
问题整改主列表 问题整改主列表
4. `issue_rectification_archive` 4. `issue_rectification_archive`
...@@ -100,8 +101,8 @@ ...@@ -100,8 +101,8 @@
对应推荐页面职责: 对应推荐页面职责:
- 驾驶舱:展示管理层首屏关注的宏观总览,保留部门分组切换,不承接编辑 - 驾驶舱:面向管理层展示宏观总览和整改进度分布,保留部门分组切换,不承接编辑
- 业务驾驶舱:展示后续业务分析图表、责任追踪、重点问题清单与质效分布,保留部门分组切换,不承接编辑 - 整改分析:展示业务分析图表、责任追踪、重点问题清单与质效分布,保留部门分组切换,不承接编辑
- 主列表:承接查询、筛选、批量操作、详情入口 - 主列表:承接查询、筛选、批量操作、详情入口
- 归档页:承接归档批次查看与明细追溯 - 归档页:承接归档批次查看与明细追溯
- 外链流程页:承接填写、复核、最终审核,不与主列表混杂 - 外链流程页:承接填写、复核、最终审核,不与主列表混杂
...@@ -146,14 +147,16 @@ ...@@ -146,14 +147,16 @@
## 5. 旧应用核心能力 ## 5. 旧应用核心能力
旧应用 `problem_rectification` 列表配置中实际暴露了 5 个操作: 当前 `problem_rectification` 列表配置暴露 8 类操作:
1. 查看详情 1. 查看详情
2. 复制数据 2. 复制数据
3. 删除数据 3. 删除数据
4. 推送问题整改表 4. 推送问题整改表
5. 导出问题整改表 5. 推送延期申请
6. 一键归档 6. 导出问题整改表
7. 一键归档
8. 状态锁定 / 解锁
这些操作分别对应如下业务含义: 这些操作分别对应如下业务含义:
...@@ -181,21 +184,38 @@ ...@@ -181,21 +184,38 @@
- 记录被推送问题的 `_id` 列表到 `restriction_value` - 记录被推送问题的 `_id` 列表到 `restriction_value`
- 将选中问题的 `metadata.fill_in_the_people` 批量更新为填报人 - 将选中问题的 `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 - 按选中问题导出 Excel
- 导出字段来自 `problem_rectification.metadata` - 导出字段来自 `problem_rectification.metadata`
- 本质是把主表字段映射成报表列,不参与状态流转 - 本质是把主表字段映射成报表列,不参与状态流转
### 5.6 一键归档 ### 5.7 一键归档
- 将选中问题记录导入 `problem_rectification_archive_records` - 将选中问题记录导入 `problem_rectification_archive_records`
- 再创建一条 `issue_rectification_archive` 记录,保存归档明细引用列表 - 再创建一条 `issue_rectification_archive` 记录,保存归档明细引用列表
- 归档的本质是生成快照,不应直接依赖原始问题记录后续继续存在 - 归档的本质是生成快照,不应直接依赖原始问题记录后续继续存在
### 5.7 旧多维表格能力盘点 ### 5.8 状态锁定 / 解锁
- 单行操作区按状态显示锁定或解锁图标按钮,按钮权限后续接入权限配置
- 只有整改进度汇总值严格为“已整改”且尚未锁定的记录显示“锁定”;“已优化”及其它状态不显示该按钮
- 锁定写入 `metadata.is_locked = true`;解锁写入 `metadata.is_locked = false`
- 锁定后所有用户只能查看记录,不允许编辑、复制、删除、推送整改、保存流程内容、送审、复核或最终审批
- 解锁后恢复原有视图权限和流程角色权限
### 5.9 旧多维表格能力盘点
实际页面检查后,旧系统“问题整改”和“往期材料归档”页都运行在同一套多维表格容器上,页面共有能力包括: 实际页面检查后,旧系统“问题整改”和“往期材料归档”页都运行在同一套多维表格容器上,页面共有能力包括:
...@@ -264,7 +284,7 @@ ...@@ -264,7 +284,7 @@
3. 根据 2026 年整改驾驶舱要求,补录扩展治理字段: 3. 根据 2026 年整改驾驶舱要求,补录扩展治理字段:
- 问题后果:是否造成经济损失、对应经济损失、是否造成外部影响、处罚机构、是否涉及员工问责、问责人数 - 问题后果:是否造成经济损失、对应经济损失、是否造成外部影响、处罚机构、是否涉及员工问责、问责人数
- 问题画像:问题综合严重程度、问题一级至四级分类、问题原因 - 问题画像:问题综合严重程度、问题一级至四级分类、问题原因
- 整改质效:整改时间评估、整改证明材料质量、是否涉及制度修订、制度名称、是否涉及系统建设、系统名称、是否为往期发现问题、问题是否重复出现、综合整改评定等级 - 整改质效:整改时间评估、整改证明材料质量、是否涉及制度修订、制度名称、是否涉及系统建设、系统名称、是否为往期发现问题、问题是否重复出现;综合整改质效结果由系统计算,不提供人工录入
- 责任识别:主责部门、实际整改完成时间、实际检查完成日期 - 责任识别:主责部门、实际整改完成时间、实际检查完成日期
4. 可预先填写预计整改完成时间 4. 可预先填写预计整改完成时间
5. 初始时可不指定填报人、复核人 5. 初始时可不指定填报人、复核人
...@@ -299,7 +319,7 @@ ...@@ -299,7 +319,7 @@
6. 单条部门推送时,回写该问题的 `metadata.department_rectification_items`;每个部门明细独立保存责任部门、填写人、审批人、整改字段、审批状态和待办 ID 6. 单条部门推送时,回写该问题的 `metadata.department_rectification_items`;每个部门明细独立保存责任部门、填写人、审批人、整改字段、审批状态和待办 ID
- 写入后重新读取父问题并校验部门明细数量;后台未返回预期明细时,本次推送显示失败,不展示成功提示 - 写入后重新读取父问题并校验部门明细数量;后台未返回预期明细时,本次推送显示失败,不展示成功提示
7. 多选简单推送时,批量更新选中问题的 `metadata.fill_in_the_people`、`metadata.review_people` 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 @@ ...@@ -374,6 +394,39 @@
- 最终审批行级处理,不要求本次推送所有部门都先审批通过 - 最终审批行级处理,不要求本次推送所有部门都先审批通过
- 发起人编辑已经送审或审批完成的父问题、部门整改内容时,不自动重置审批状态,也不自动重新生成 OA 待办 - 发起人编辑已经送审或审批完成的父问题、部门整改内容时,不自动重置审批状态,也不自动重新生成 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. 状态流转约定 ## 8. 状态流转约定
...@@ -402,7 +455,7 @@ ...@@ -402,7 +455,7 @@
- `sender` 记录推送发起人 - `sender` 记录推送发起人
- `editor` 记录填写人列表 - `editor` 记录填写人列表
- `auditor` 记录审核人列表 - `auditor` 记录审核人列表
- `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`
- `restriction_key` 默认固定为 `_id` - `restriction_key` 默认固定为 `_id`
- `restriction_value` 保存本次推送绑定的整改记录 `_id[]` - `restriction_value` 保存本次推送绑定的整改记录 `_id[]`
...@@ -427,14 +480,31 @@ ...@@ -427,14 +480,31 @@
- 新流程页中,填写人只能维护本人负责行的整改业务字段,送审后锁定,复核驳回后恢复;复核驳回状态下对应审批人可以维护该行整改业务字段,审批通过后锁定,且审批人不能点击填写人的“送审”;流程推送人可以维护本次推送关联的父问题记录和全部部门明细整改业务字段,但不能直接编辑流程状态与待办 ID - 新流程页中,填写人只能维护本人负责行的整改业务字段,送审后锁定,复核驳回后恢复;复核驳回状态下对应审批人可以维护该行整改业务字段,审批通过后锁定,且审批人不能点击填写人的“送审”;流程推送人可以维护本次推送关联的父问题记录和全部部门明细整改业务字段,但不能直接编辑流程状态与待办 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. 驾驶舱展示规则 ## 9. 驾驶舱展示规则
当前驾驶舱能力以 `problem_rectification` 为唯一数据源,并拆分为两个菜单入口: 当前统计能力以 `problem_rectification` 为唯一数据源,并拆分为两个菜单入口:
- `integrity_risk_table/dashboard`:问题整改驾驶舱,仅展示顶部横幅、部门分组切换与宏观总览 KPI - `integrity_risk_table/dashboard`:问题整改驾驶舱,面向管理层展示顶部横幅、部门分组切换、宏观总览 KPI 与整改进度分布
- `integrity_risk_table/business_dashboard`:业务驾驶舱,展示原宏观总览之后的业务分析内容,并继续支持部门分组切换 - `integrity_risk_table/business_dashboard`:问题整改分析,展示问题风险定位、责任追踪与整改质效内容,并继续支持部门分组切换;为兼容历史链接保留原路由
围绕 2026 年整改驾驶舱需求,整体统计能力分为四层: 围绕 2026 年整改驾驶舱需求,整体统计能力分为四层:
...@@ -468,11 +538,11 @@ ...@@ -468,11 +538,11 @@
当前实现约定: 当前实现约定:
- 两个驾驶舱入口顶部均保留按 `feedback_unit` 分组的轻量 tabs - 两个统计入口顶部均保留按 `feedback_unit` 分组的轻量 tabs
- 两个驾驶舱入口顶部均提供“统计年份”选择器,默认“全部年份”,选择具体年份后仅统计该年份内的问题记录 - 两个统计入口顶部均提供“统计年份”选择器,默认“全部年份”,选择具体年份后仅统计该年份内的问题记录
- 问题整改驾驶舱的 tabs 下方只展示宏观总览 KPI 卡片 - 问题整改驾驶舱的 tabs 下方展示宏观总览 KPI 卡片和整改进度分布
- 业务驾驶舱的 tabs 下方展示整改进度分布、业务风险分析、责任追踪和整改质效图表 - 问题整改分析的 tabs 下方展示业务风险分析、责任追踪和整改质效图表
- 两个驾驶舱均仅负责统计展示,不承接编辑动作 - 两个统计入口均仅负责统计展示,不承接编辑动作
- 如果线上 Subject 后续把新增字段从 `text` 升级为 `select` / `date` / `boolean`,驾驶舱统计口径无需调整,只需复用标准字段值 - 如果线上 Subject 后续把新增字段从 `text` 升级为 `select` / `date` / `boolean`,驾驶舱统计口径无需调整,只需复用标准字段值
### 9.1.1 统计年份筛选 ### 9.1.1 统计年份筛选
...@@ -500,7 +570,7 @@ ...@@ -500,7 +570,7 @@
- 以 `rectification_progress` 为维度统计数量和占比 - 以 `rectification_progress` 为维度统计数量和占比
- 当前页面使用环形图表达,不再沿用旧版折线趋势表达 - 当前页面使用环形图表达,不再沿用旧版折线趋势表达
- 业务驾驶舱仅保留整改进度分布图,不再展示单独的“进度口径说明”卡片 - 问题整改驾驶舱展示整改进度分布图,不再展示单独的“进度口径说明”卡片
### 9.3 问题分析与风险定位层 ### 9.3 问题分析与风险定位层
...@@ -525,7 +595,7 @@ ...@@ -525,7 +595,7 @@
- X 轴:责任部门问题总数 - X 轴:责任部门问题总数
- Y 轴:整改完成率 - Y 轴:整改完成率
- 气泡大小:逾期问题数 - 气泡大小:逾期问题数
- 业务驾驶舱不再展示单独的“整改时效分析”散点图 - 问题整改分析不再展示单独的“整改时效分析”散点图
当前逾期 / 延期判定说明: 当前逾期 / 延期判定说明:
...@@ -539,7 +609,7 @@ ...@@ -539,7 +609,7 @@
当前图表与字段映射: 当前图表与字段映射:
- 驾驶舱只展示最终“综合整改质效结果”的高 / 中 / 低分布,不再展示整改时间评估、整改证明材料质量、制度修订、系统建设、往期发现、重复出现等明细维度 - 问题整改分析只展示最终“综合整改质效结果”的高 / 中 / 低分布,不再展示整改时间评估、整改证明材料质量、制度修订、系统建设、往期发现、重复出现等明细维度
- 明细维度仍在维护页录入和校验,包括: - 明细维度仍在维护页录入和校验,包括:
- `rectification_timeliness_level`:整改时间评估,低 / 中 / 高 - `rectification_timeliness_level`:整改时间评估,低 / 中 / 高
- `rectification_material_quality_level`:整改证明材料质量,低 / 中 / 高 - `rectification_material_quality_level`:整改证明材料质量,低 / 中 / 高
...@@ -547,8 +617,12 @@ ...@@ -547,8 +617,12 @@
- `involves_system_construction`、`system_construction_name`:是否涉及系统建设及系统名称 - `involves_system_construction`、`system_construction_name`:是否涉及系统建设及系统名称
- `is_historical_problem`:是否为往期发现问题 - `is_historical_problem`:是否为往期发现问题
- `is_repeated_problem`:问题是否重复出现 - `is_repeated_problem`:问题是否重复出现
- `rectification_overall_rating`:综合整改评定等级,低 / 中 / 高 - `rectification_overall_rating`:历史兼容字段,新建和统计均不再读取或写入
- 当前驾驶舱优先使用已维护的 `rectification_overall_rating`;若该字段为空,则按初步规则从明细维度推导结果:两个及以上高为高;出现低则低;一个高则中。具体参与维度和最终规则后续需业务确认 - 综合整改质效结果由系统实时计算,不写回 `rectification_overall_rating`:
- 整改时间评估或整改证明材料质量任一项为“高”,或者四个“是否”字段任一项为“是”时,结果为“高”;高等级优先
- 未出现“高”或“是”,且整改时间评估或整改证明材料质量任一项为“低”时,结果为“低”
- 已存在至少一个有效评估维度、但未满足高或低规则时,结果为“中”
- 六个参与字段均无有效值时,不生成综合整改质效结果
--- ---
......
...@@ -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);
......
...@@ -5,7 +5,10 @@ import { ...@@ -5,7 +5,10 @@ import {
loadSubject, loadSubject,
queryRecords, queryRecords,
} from '@/services/DataService'; } from '@/services/DataService';
import { DEPARTMENT_GROUPS } from './problemRectificationDashboardService'; import {
calculateRectificationQualityRating,
DEPARTMENT_GROUPS,
} from './problemRectificationDashboardService';
import { import {
buildViewColumns, buildViewColumns,
checkViewCondition, checkViewCondition,
...@@ -25,8 +28,11 @@ export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification'; ...@@ -25,8 +28,11 @@ export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification';
export const PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]; export const PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
export const DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE = 20; export const DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE = 20;
export const DEPARTMENT_RECTIFICATION_FIELD = 'department_rectification_items'; 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_DEPARTMENT = 'department';
export const FLOW_MODE_BATCH = 'batch'; 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 PROBLEM_RECTIFICATION_FLOW_TABLE_PATH = '/problem_rectification_flow_table';
export const EDITOR_STATUS_PENDING = '未完成'; export const EDITOR_STATUS_PENDING = '未完成';
export const EDITOR_STATUS_COMPLETED = '已完成'; export const EDITOR_STATUS_COMPLETED = '已完成';
...@@ -39,7 +45,6 @@ const FILTER_FIELD_PATHS = { ...@@ -39,7 +45,6 @@ const FILTER_FIELD_PATHS = {
progress: 'metadata.rectification_progress', progress: 'metadata.rectification_progress',
auditStatus: 'metadata.audit_status', auditStatus: 'metadata.audit_status',
severityLevel: 'metadata.problem_severity_level', severityLevel: 'metadata.problem_severity_level',
overallRating: 'metadata.rectification_overall_rating',
primaryResponsibleDepartment: 'metadata.primary_responsible_department', primaryResponsibleDepartment: 'metadata.primary_responsible_department',
problemFrom: 'metadata.problem_from', problemFrom: 'metadata.problem_from',
rootCause: 'metadata.problem_root_cause', rootCause: 'metadata.problem_root_cause',
...@@ -66,7 +71,6 @@ const WORKBENCH_PRIORITY_CREATE_FIELD_NAMES = [ ...@@ -66,7 +71,6 @@ const WORKBENCH_PRIORITY_CREATE_FIELD_NAMES = [
'system_construction_name', 'system_construction_name',
'is_historical_problem', 'is_historical_problem',
'is_repeated_problem', 'is_repeated_problem',
'rectification_overall_rating',
'actual_rectification_completed_at', 'actual_rectification_completed_at',
'inspection_completed_at', 'inspection_completed_at',
'primary_responsible_department', 'primary_responsible_department',
...@@ -151,13 +155,7 @@ export function getProblemRectificationFlowTableUrl(messageName) { ...@@ -151,13 +155,7 @@ export function getProblemRectificationFlowTableUrl(messageName) {
const query = messageName ? `?id=${encodeURIComponent(messageName)}` : ''; const query = messageName ? `?id=${encodeURIComponent(messageName)}` : '';
const basePath = String(PROBLEM_RECTIFICATION_APP_BASE_PATH || '/') const basePath = String(PROBLEM_RECTIFICATION_APP_BASE_PATH || '/')
.replace(/\/+$/, ''); .replace(/\/+$/, '');
const appPath = `${basePath}${PROBLEM_RECTIFICATION_FLOW_TABLE_PATH}${query}`; return `${basePath}${PROBLEM_RECTIFICATION_FLOW_TABLE_PATH}${query}`;
if (typeof window === 'undefined') {
return appPath;
}
return `${window.location.origin}${appPath}`;
} }
export function getProblemRectificationFormUrl(messageName) { export function getProblemRectificationFormUrl(messageName) {
...@@ -189,6 +187,7 @@ export function normalizeDepartmentRectificationItems(record) { ...@@ -189,6 +187,7 @@ export function normalizeDepartmentRectificationItems(record) {
return toArray(parsedItems) return toArray(parsedItems)
.filter((item) => item && typeof item === 'object') .filter((item) => item && typeof item === 'object')
.map((item, index) => ({ .map((item, index) => ({
...item,
item_id: normalizeText(item.item_id) || makeDepartmentItemId(record, item.responsible_department, index), item_id: normalizeText(item.item_id) || makeDepartmentItemId(record, item.responsible_department, index),
responsible_department: normalizeText(item.responsible_department), responsible_department: normalizeText(item.responsible_department),
editor: normalizeUserId(item.editor), editor: normalizeUserId(item.editor),
...@@ -210,6 +209,56 @@ export function normalizeDepartmentRectificationItems(record) { ...@@ -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) { export function buildDepartmentAssignmentsFromRecord(record) {
const metadata = record?.metadata || record?.raw?.metadata || {}; const metadata = record?.metadata || record?.raw?.metadata || {};
const departments = uniqueTruthy( const departments = uniqueTruthy(
...@@ -449,6 +498,19 @@ function buildKeywordFilter(keyword, columns = []) { ...@@ -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) { function buildFocusFlagFilter(focusFlag) {
if (focusFlag === 'consequence') { if (focusFlag === 'consequence') {
return { return {
...@@ -465,7 +527,7 @@ function buildFocusFlagFilter(focusFlag) { ...@@ -465,7 +527,7 @@ function buildFocusFlagFilter(focusFlag) {
} }
if (focusFlag === 'low_rating') { if (focusFlag === 'low_rating') {
return { 'metadata.rectification_overall_rating': '低' }; return buildCalculatedQualityRatingFilter('低');
} }
if (focusFlag === 'repeated_or_historical') { if (focusFlag === 'repeated_or_historical') {
...@@ -496,6 +558,77 @@ function buildFocusFlagFilter(focusFlag) { ...@@ -496,6 +558,77 @@ function buildFocusFlagFilter(focusFlag) {
return {}; 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) { export function buildProblemRectificationQueryFilter(filters = {}, gridView, subject) {
const columns = buildViewColumnsForList(gridView, subject); const columns = buildViewColumnsForList(gridView, subject);
const uiFieldClauses = Object.entries(FILTER_FIELD_PATHS).map(([filterKey, fieldPath]) => { const uiFieldClauses = Object.entries(FILTER_FIELD_PATHS).map(([filterKey, fieldPath]) => {
...@@ -511,6 +644,8 @@ export function buildProblemRectificationQueryFilter(filters = {}, gridView, sub ...@@ -511,6 +644,8 @@ export function buildProblemRectificationQueryFilter(filters = {}, gridView, sub
buildRestrictionsFilter(gridView?.settings?.restrictions), buildRestrictionsFilter(gridView?.settings?.restrictions),
buildViewConditionsFilter(gridView?.settings?.filters), buildViewConditionsFilter(gridView?.settings?.filters),
...uiFieldClauses, ...uiFieldClauses,
buildInspectionReportMonthFilter(filters?.inspectionReportMonth),
buildCalculatedQualityRatingFilter(filters?.overallRating),
buildFocusFlagFilter(filters?.focusFlag), buildFocusFlagFilter(filters?.focusFlag),
buildKeywordFilter(filters?.keyword, columns), buildKeywordFilter(filters?.keyword, columns),
]); ]);
...@@ -572,12 +707,13 @@ export function normalizeProblemRectificationRecord(record) { ...@@ -572,12 +707,13 @@ export function normalizeProblemRectificationRecord(record) {
const isOverdue = const isOverdue =
dueDate && dueDate.isValid() ? dueDate.isBefore(now, 'day') && !isFixed : false; dueDate && dueDate.isValid() ? dueDate.isBefore(now, 'day') && !isFixed : false;
return { const normalizedRecord = {
id: record?._id || record?.name || '', id: record?._id || record?.name || '',
name: record?.name || '', name: record?.name || '',
title: normalizeText(record?.title) || normalizeText(metadata.project_name), title: normalizeText(record?.title) || normalizeText(metadata.project_name),
projectName: normalizeText(metadata.project_name) || '-', projectName: normalizeText(metadata.project_name) || '-',
inspectUnit: normalizeText(metadata.inspect_the_implementing_unit), inspectUnit: normalizeText(metadata.inspect_the_implementing_unit),
inspectionReportDate: normalizeText(metadata.inspection_report_date),
progress: normalizedProgress, progress: normalizedProgress,
feedbackUnit: normalizeText(metadata.feedback_unit), feedbackUnit: normalizeText(metadata.feedback_unit),
auditStatus: summarizedAuditStatus, auditStatus: summarizedAuditStatus,
...@@ -594,7 +730,6 @@ export function normalizeProblemRectificationRecord(record) { ...@@ -594,7 +730,6 @@ export function normalizeProblemRectificationRecord(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: normalizeText(metadata.primary_responsible_department), primaryResponsibleDepartment: normalizeText(metadata.primary_responsible_department),
hasEconomicLoss, hasEconomicLoss,
hasEconomicLossLabel: normalizeFlagText(metadata.has_economic_loss), hasEconomicLossLabel: normalizeFlagText(metadata.has_economic_loss),
...@@ -635,10 +770,17 @@ export function normalizeProblemRectificationRecord(record) { ...@@ -635,10 +770,17 @@ export function normalizeProblemRectificationRecord(record) {
dueDateLabel: formatDateText(summarizedDueDate) || (hasDepartmentItems ? '见部门明细' : '-'), dueDateLabel: formatDateText(summarizedDueDate) || (hasDepartmentItems ? '见部门明细' : '-'),
departmentItems, departmentItems,
hasDepartmentItems, hasDepartmentItems,
delayRequest: normalizeDelayRequest(metadata[DELAY_REQUEST_FIELD]),
isFixed, isFixed,
isLocked: metadata.is_locked === true,
isOverdue, isOverdue,
raw: record, raw: record,
}; };
return {
...normalizedRecord,
overallRating: calculateRectificationQualityRating(normalizedRecord),
};
} }
export async function loadProblemRectificationListData({ export async function loadProblemRectificationListData({
...@@ -738,6 +880,7 @@ export function getRecordCellText(record, column) { ...@@ -738,6 +880,7 @@ export function getRecordCellText(record, column) {
export function buildProblemRectificationConfiguredListView(records, filters, gridView, subject) { export function buildProblemRectificationConfiguredListView(records, filters, gridView, subject) {
const nextKeyword = filters?.keyword || ''; const nextKeyword = filters?.keyword || '';
const nextInspectionReportMonth = normalizeText(filters?.inspectionReportMonth);
const nextProgress = filters?.progress || ''; const nextProgress = filters?.progress || '';
const nextAuditStatus = filters?.auditStatus || ''; const nextAuditStatus = filters?.auditStatus || '';
const nextSeverityLevel = filters?.severityLevel || ''; const nextSeverityLevel = filters?.severityLevel || '';
...@@ -751,6 +894,14 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr ...@@ -751,6 +894,14 @@ export function buildProblemRectificationConfiguredListView(records, filters, gr
const searchableFields = columns.map((column) => column.field); const searchableFields = columns.map((column) => column.field);
const filteredRecords = records.filter((record) => { const filteredRecords = records.filter((record) => {
if (
nextInspectionReportMonth &&
!record.inspectionReportDate.startsWith(`${nextInspectionReportMonth}-`) &&
record.inspectionReportDate !== nextInspectionReportMonth
) {
return false;
}
const rawRecord = record.raw || record; const rawRecord = record.raw || record;
if (!recordMatchesRestrictions(rawRecord, gridView?.settings?.restrictions)) { if (!recordMatchesRestrictions(rawRecord, gridView?.settings?.restrictions)) {
...@@ -1002,7 +1153,13 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) { ...@@ -1002,7 +1153,13 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) {
const columns = buildViewColumnsForList(gridView, subject) const columns = buildViewColumnsForList(gridView, subject)
.filter((column) => column.field !== 'title') .filter((column) => column.field !== 'title')
.filter((column) => column.field?.startsWith('metadata.')) .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 seen = new Set();
const fields = columns.filter((column) => { const fields = columns.filter((column) => {
......
...@@ -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