Commit 5a67a51e authored by David Yang's avatar David Yang

feat: 完善批量整改流程和严重程度计算

parent 1cf5afe5
This diff is collapsed.
allowBuilds:
core-js: false
core-js-pure: false
es5-ext: false
esbuild: false
{ {
"name": "problem_rectification_new", "name": "problem_rectification_new",
"version": "1.0.6", "version": "1.0.7",
"build": "abc", "build": "abc",
"description": "merge master", "description": "merge master",
"date": "2025-07-14 19:27:20", "date": "2025-07-14 19:27:20",
......
...@@ -40,7 +40,27 @@ function transformSaveValue(newValue, fieldDef) { ...@@ -40,7 +40,27 @@ function transformSaveValue(newValue, fieldDef) {
return newValue; return newValue;
} }
export function useSubjectRecord(recordName, refreshKey, readonly, subjectName) { function buildNestedPayload(fieldPath, value) {
return fieldPath.split('.').reduceRight((nestedValue, key) => ({ [key]: nestedValue }), value);
}
function mergePayload(target, source) {
return Object.entries(source || {}).reduce((result, [key, value]) => ({
...result,
[key]:
value && typeof value === 'object' && !Array.isArray(value)
? mergePayload(result[key] || {}, value)
: value,
}), target);
}
export function useSubjectRecord(
recordName,
refreshKey,
readonly,
subjectName,
deriveFieldValues,
) {
const [remoteRecord, setRemoteRecord] = useState(null); const [remoteRecord, setRemoteRecord] = useState(null);
const [editableRecord, setEditableRecord] = useState(null); const [editableRecord, setEditableRecord] = useState(null);
const [subject, setSubject] = useState(null); const [subject, setSubject] = useState(null);
...@@ -107,12 +127,24 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName) ...@@ -107,12 +127,24 @@ export function useSubjectRecord(recordName, refreshKey, readonly, subjectName)
} }
const previousRecord = editableRecord; const previousRecord = editableRecord;
const nextRecord = setFieldValue(editableRecord, fieldPath, newValue); let nextRecord = setFieldValue(editableRecord, fieldPath, newValue);
setEditableRecord(nextRecord);
try { try {
const saveValue = transformSaveValue(newValue, fieldDef); const saveValue = transformSaveValue(newValue, fieldDef);
const payload = fieldPath.split('.').reduceRight((value, key) => ({ [key]: value }), saveValue); let payload = buildNestedPayload(fieldPath, saveValue);
const derivedFieldValues = deriveFieldValues?.({
fieldPath,
fieldDef,
newValue: saveValue,
record: nextRecord,
}) || {};
Object.entries(derivedFieldValues).forEach(([derivedFieldPath, derivedValue]) => {
nextRecord = setFieldValue(nextRecord, derivedFieldPath, derivedValue);
payload = mergePayload(payload, buildNestedPayload(derivedFieldPath, derivedValue));
});
setEditableRecord(nextRecord);
await updateRecord(recordName, payload); await updateRecord(recordName, payload);
setRemoteRecord(nextRecord); setRemoteRecord(nextRecord);
} catch (updateError) { } catch (updateError) {
......
...@@ -51,6 +51,7 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -51,6 +51,7 @@ const RecordDetailDrawer = (props, ref) => {
preferredViewName, preferredViewName,
disabledFields = EMPTY_ARRAY, disabledFields = EMPTY_ARRAY,
authorizationView = null, authorizationView = null,
deriveFieldValues,
} = props; } = props;
const [refreshKey, setRefreshKey] = useState(() => Date.now()); const [refreshKey, setRefreshKey] = useState(() => Date.now());
...@@ -66,7 +67,13 @@ const RecordDetailDrawer = (props, ref) => { ...@@ -66,7 +67,13 @@ const RecordDetailDrawer = (props, ref) => {
handleFieldUpdate, handleFieldUpdate,
isFieldDisabled, isFieldDisabled,
loading, loading,
} = useSubjectRecord(recordName, refreshKey, readonly, subjectName); } = useSubjectRecord(
recordName,
refreshKey,
readonly,
subjectName,
deriveFieldValues,
);
useEffect(() => { useEffect(() => {
if (open) { if (open) {
......
...@@ -464,6 +464,32 @@ function buildFocusIssueBarOption(data = []) { ...@@ -464,6 +464,32 @@ function buildFocusIssueBarOption(data = []) {
function buildBubbleMatrixOption(data = []) { function buildBubbleMatrixOption(data = []) {
const maxTotal = Math.max(...data.map((item) => item.totalCount), 1); const maxTotal = Math.max(...data.map((item) => item.totalCount), 1);
const maxOverdue = Math.max(...data.map((item) => item.overdueCount), 1); const maxOverdue = Math.max(...data.map((item) => item.overdueCount), 1);
const xAxisMax = maxTotal + Math.max(1, Math.ceil(maxTotal * 0.08));
const coordinateOccurrences = new Map();
const chartData = data.map((item) => {
const coordinateKey = `${item.totalCount}:${item.completionRate}`;
const labelStackIndex = coordinateOccurrences.get(coordinateKey) || 0;
coordinateOccurrences.set(coordinateKey, labelStackIndex + 1);
return {
...item,
value: [item.totalCount, item.completionRate],
symbolSize: 16 + (item.overdueCount / maxOverdue) * 34,
itemStyle: {
color:
item.completionRate >= 75
? '#22c55e'
: item.completionRate >= 45
? '#f59e0b'
: '#e11d48',
opacity: 0.85,
},
label: {
position: item.completionRate >= 75 ? 'bottom' : 'top',
distance: 8 + labelStackIndex * 18,
},
};
});
return { return {
tooltip: { tooltip: {
...@@ -481,15 +507,17 @@ function buildBubbleMatrixOption(data = []) { ...@@ -481,15 +507,17 @@ function buildBubbleMatrixOption(data = []) {
textStyle: { color: '#fff' }, textStyle: { color: '#fff' },
}, },
grid: { grid: {
left: 8, left: 12,
right: 16, right: 24,
top: 12, top: 28,
bottom: 10, bottom: 16,
containLabel: true, containLabel: true,
}, },
xAxis: { xAxis: {
type: 'value', type: 'value',
name: '问题总数', name: '问题总数',
min: 0,
max: xAxisMax,
nameTextStyle: { color: '#64748b' }, nameTextStyle: { color: '#64748b' },
splitLine: { lineStyle: { color: 'rgba(148,163,184,0.18)' } }, splitLine: { lineStyle: { color: 'rgba(148,163,184,0.18)' } },
axisLabel: { color: '#64748b' }, axisLabel: { color: '#64748b' },
...@@ -499,33 +527,41 @@ function buildBubbleMatrixOption(data = []) { ...@@ -499,33 +527,41 @@ function buildBubbleMatrixOption(data = []) {
name: '整改完成率(%)', name: '整改完成率(%)',
nameTextStyle: { color: '#64748b' }, nameTextStyle: { color: '#64748b' },
min: 0, min: 0,
max: 100, max: 105,
splitLine: { lineStyle: { color: 'rgba(148,163,184,0.18)' } }, splitLine: { lineStyle: { color: 'rgba(148,163,184,0.18)' } },
axisLabel: { color: '#64748b' }, axisLabel: { color: '#64748b' },
}, },
series: [ series: [
{ {
type: 'scatter', type: 'scatter',
data: data.map((item) => ({ data: chartData,
...item,
value: [item.totalCount, item.completionRate],
symbolSize: 16 + (item.overdueCount / maxOverdue) * 34,
itemStyle: {
color:
item.completionRate >= 75
? '#22c55e'
: item.completionRate >= 45
? '#f59e0b'
: '#e11d48',
opacity: 0.85,
},
})),
label: { label: {
show: true, show: true,
formatter: (params) => params.data.name, formatter: (params) => params.data.name,
position: 'top', position: 'top',
distance: 8,
color: '#334155', color: '#334155',
fontSize: 12, fontSize: 12,
backgroundColor: 'rgba(255,255,255,0.9)',
borderRadius: 3,
padding: [2, 4],
},
labelLayout: {
moveOverlap: 'shiftY',
hideOverlap: true,
},
emphasis: {
focus: 'self',
label: {
show: true,
color: '#0f172a',
fontWeight: 700,
backgroundColor: '#fff',
borderColor: '#cbd5e1',
borderWidth: 1,
borderRadius: 3,
padding: [3, 5],
},
}, },
}, },
], ],
......
...@@ -78,6 +78,10 @@ import { ...@@ -78,6 +78,10 @@ import {
normalizeDelayRequestItems, normalizeDelayRequestItems,
normalizeDepartmentRectificationItems, normalizeDepartmentRectificationItems,
} from '../problemRectificationListService'; } from '../problemRectificationListService';
import {
calculateProblemSeverityLevel,
PROBLEM_SEVERITY_SOURCE_FIELDS,
} from '../problemSeverity';
const FIELD_HINTS = { const FIELD_HINTS = {
inspect_the_implementing_unit: '按对象配置的实施单位选项选择', inspect_the_implementing_unit: '按对象配置的实施单位选项选择',
...@@ -92,7 +96,7 @@ const FIELD_HINTS = { ...@@ -92,7 +96,7 @@ const FIELD_HINTS = {
has_external_impact: '建议统一填写“是”或“否”', has_external_impact: '建议统一填写“是”或“否”',
has_employee_accountability: '建议统一填写“是”或“否”', has_employee_accountability: '建议统一填写“是”或“否”',
accountability_person_count: '建议填写数字人数', accountability_person_count: '建议填写数字人数',
problem_severity_level: '建议统一填写“高 / 中 / 低”', problem_severity_level: '由经济损失、外部影响和员工问责情况自动计算',
rectification_timeliness_level: '建议统一填写“高 / 中 / 低”', rectification_timeliness_level: '建议统一填写“高 / 中 / 低”',
rectification_material_quality_level: '建议统一填写“高 / 中 / 低”', rectification_material_quality_level: '建议统一填写“高 / 中 / 低”',
involves_policy_revision: '建议统一填写“是”或“否”', involves_policy_revision: '建议统一填写“是”或“否”',
...@@ -112,7 +116,6 @@ const CREATE_FIELD_OPTION_OVERRIDES = { ...@@ -112,7 +116,6 @@ const CREATE_FIELD_OPTION_OVERRIDES = {
has_economic_loss: ['是', '否'], has_economic_loss: ['是', '否'],
has_external_impact: ['是', '否'], has_external_impact: ['是', '否'],
has_employee_accountability: ['是', '否'], has_employee_accountability: ['是', '否'],
problem_severity_level: ['高', '中', '低'],
problem_category_level_1: ['内控建设', '内控运行', '内控建设及运行', '其他'], problem_category_level_1: ['内控建设', '内控运行', '内控建设及运行', '其他'],
problem_category_level_2: ['公司治理', '投资管理', '销售管理', '员工行为管理', '基金运营', '财务管理', '信息技术'], problem_category_level_2: ['公司治理', '投资管理', '销售管理', '员工行为管理', '基金运营', '财务管理', '信息技术'],
problem_root_cause: ['内外规执行不到位', '系统化程度低', '规矩意识淡薄导致操作风险', '现有流程设计缺陷', '制度建设有待完善'], problem_root_cause: ['内外规执行不到位', '系统化程度低', '规矩意识淡薄导致操作风险', '现有流程设计缺陷', '制度建设有待完善'],
...@@ -405,6 +408,10 @@ function CreateField({ fieldDef, value, onChange }) { ...@@ -405,6 +408,10 @@ function CreateField({ fieldDef, value, onChange }) {
const placeholder = FIELD_HINTS[fieldDef?.name] || `请输入${fieldDef?.label || fieldDef?.name}`; const placeholder = FIELD_HINTS[fieldDef?.name] || `请输入${fieldDef?.label || fieldDef?.name}`;
const overrideOptions = CREATE_FIELD_OPTION_OVERRIDES[fieldDef?.name] || []; const overrideOptions = CREATE_FIELD_OPTION_OVERRIDES[fieldDef?.name] || [];
if (fieldDef?.name === 'problem_severity_level') {
return <Input value={value || '低'} disabled aria-label="问题综合严重程度(自动计算)" />;
}
if (fieldType === 'long_text') { if (fieldType === 'long_text') {
return ( return (
<Textarea <Textarea
...@@ -1289,6 +1296,39 @@ export default function ProblemRectificationListPage() { ...@@ -1289,6 +1296,39 @@ export default function ProblemRectificationListPage() {
const nextAuditorIds = isDepartmentFlow const nextAuditorIds = isDepartmentFlow
? Array.from(new Set(departmentAssignments.map((item) => item.auditor?._id).filter(Boolean))) ? Array.from(new Set(departmentAssignments.map((item) => item.auditor?._id).filter(Boolean)))
: auditorIds; : auditorIds;
const batchDepartmentItemsByRecordId = new Map();
if (!isDepartmentFlow) {
const now = dayjs().toISOString();
for (const selectedRecord of selectedRecords) {
const sourceRecord = await loadRecord(selectedRecord.id);
const existingItems = normalizeDepartmentRectificationItems(sourceRecord);
const nextItems = existingItems.length > 0
? existingItems.map((item) => ({
...item,
editor: nextFillerIds[0],
auditor: nextAuditorIds[0],
editor_status: EDITOR_STATUS_PENDING,
audit_status: AUDIT_STATUS_PENDING,
final_audit_status: AUDIT_STATUS_PENDING,
progress_id_editor: '',
progress_id_auditor: '',
progress_id_sender: '',
updated_by: currentUserInfo?._id || currentUserInfo?.id || '',
updated_at: now,
}))
: buildDepartmentRectificationItems(
sourceRecord,
buildDepartmentAssignmentsFromRecord(sourceRecord).map((assignment) => ({
...assignment,
editor: { _id: nextFillerIds[0] },
auditor: { _id: nextAuditorIds[0] },
})),
currentUserInfo,
);
batchDepartmentItemsByRecordId.set(selectedRecord.id, nextItems);
}
}
const messageRecord = await createRecord({ const messageRecord = await createRecord({
subject: 'message_reference_id', subject: 'message_reference_id',
...@@ -1303,7 +1343,11 @@ export default function ProblemRectificationListPage() { ...@@ -1303,7 +1343,11 @@ export default function ProblemRectificationListPage() {
view_name: activeGridView?.name || '', view_name: activeGridView?.name || '',
subject_name: PROBLEM_RECTIFICATION_SUBJECT, subject_name: PROBLEM_RECTIFICATION_SUBJECT,
flow_mode: isDepartmentFlow ? FLOW_MODE_DEPARTMENT : FLOW_MODE_BATCH, flow_mode: isDepartmentFlow ? FLOW_MODE_DEPARTMENT : FLOW_MODE_BATCH,
department_item_ids: isDepartmentFlow ? departmentAssignments.map((item) => item.item_id) : [], department_item_ids: isDepartmentFlow
? departmentAssignments.map((item) => item.item_id)
: Array.from(batchDepartmentItemsByRecordId.values())
.flat()
.map((item) => item.item_id),
editor_status: EDITOR_STATUS_PENDING, editor_status: EDITOR_STATUS_PENDING,
auditor_status: EDITOR_STATUS_PENDING, auditor_status: EDITOR_STATUS_PENDING,
sender_status: EDITOR_STATUS_PENDING, sender_status: EDITOR_STATUS_PENDING,
...@@ -1356,7 +1400,7 @@ export default function ProblemRectificationListPage() { ...@@ -1356,7 +1400,7 @@ export default function ProblemRectificationListPage() {
} else { } else {
const fillerProfile = await loadUser(nextFillerIds[0]); const fillerProfile = await loadUser(nextFillerIds[0]);
const oaProcessResult = await executeAction('oa_notice:createProgress', { const oaProcessResult = await executeAction('oa_notice:createProgress', {
title: `${dayjs().format('YYYY-MM-DD')} 填写问题整改表`, title: `${dayjs().format('YYYY-MM-DD')} 填写问题整改表(${selectedRecordIds.length} 条)`,
classify: '流程待办', classify: '流程待办',
contentType: '流程待办', contentType: '流程待办',
receiveUserId: fillerProfile._id, receiveUserId: fillerProfile._id,
...@@ -1368,20 +1412,32 @@ export default function ProblemRectificationListPage() { ...@@ -1368,20 +1412,32 @@ export default function ProblemRectificationListPage() {
returnUrl, returnUrl,
}); });
progressIdEditor.push(oaProcessResult?.data?.taskId); progressIdEditor.push(oaProcessResult?.data?.taskId);
const batchProgressIdEditor = oaProcessResult?.data?.taskId || '';
for (const selectedRecord of selectedRecords) {
const departmentItems = batchDepartmentItemsByRecordId.get(selectedRecord.id) || [];
const nextItems = departmentItems.map((item) => ({
...item,
progress_id_editor: batchProgressIdEditor,
}));
await batchUpdateRecords({
subject: PROBLEM_RECTIFICATION_SUBJECT,
filter: { _id: selectedRecord.id },
data: buildDepartmentParentBatchUpdateData(nextItems),
});
await batchUpdateRecords({ const savedRecord = await loadRecord(selectedRecord.id);
subject: PROBLEM_RECTIFICATION_SUBJECT, const savedItems = normalizeDepartmentRectificationItems(savedRecord);
filter: { const expectedItemIds = new Set(nextItems.map((item) => item.item_id));
_id: { $in: selectedRecordIds }, if (
}, savedItems.length !== nextItems.length ||
data: { savedItems.some((item) => !expectedItemIds.has(item.item_id))
'metadata.fill_in_the_people': nextFillerIds, ) {
'metadata.review_people': nextAuditorIds, throw new Error(
'metadata.audit_status': AUDIT_STATUS_PENDING, `批量推送明细写入失败:问题“${selectedRecord.title || selectedRecord.id}”预期 ${nextItems.length} 条,后台实际返回 ${savedItems.length} 条。`,
'metadata.final_audit_status': AUDIT_STATUS_PENDING, );
[`metadata.${DEPARTMENT_RECTIFICATION_FIELD}`]: [], }
}, }
});
} }
await updateRecord(messageRecord._id || messageRecord.name, { await updateRecord(messageRecord._id || messageRecord.name, {
...@@ -1410,7 +1466,7 @@ export default function ProblemRectificationListPage() { ...@@ -1410,7 +1466,7 @@ export default function ProblemRectificationListPage() {
setSuccessMessage( setSuccessMessage(
isDepartmentFlow isDepartmentFlow
? `推送成功,已生成 ${departmentAssignments.length} 条部门整改明细,可从记录标题右侧展开查看。` ? `推送成功,已生成 ${departmentAssignments.length} 条部门整改明细,可从记录标题右侧展开查看。`
: `推送成功,已将 ${selectedRecords.length} 条问题发送给填写人。`, : `推送成功,已将 ${selectedRecords.length} 条问题的 ${Array.from(batchDepartmentItemsByRecordId.values()).flat().length} 条部门整改明细发送给填写人。`,
); );
} catch (requestError) { } catch (requestError) {
setError(requestError?.message || '推送问题整改表失败,请稍后重试。'); setError(requestError?.message || '推送问题整改表失败,请稍后重试。');
...@@ -1636,7 +1692,23 @@ export default function ProblemRectificationListPage() { ...@@ -1636,7 +1692,23 @@ export default function ProblemRectificationListPage() {
recordName={recordName} recordName={recordName}
onClose={onClose} onClose={onClose}
onDataChange={onDataChange} onDataChange={onDataChange}
disabledFields={[DELAY_REQUEST_FIELD, DELAY_REQUEST_ITEMS_FIELD]} disabledFields={[
DELAY_REQUEST_FIELD,
DELAY_REQUEST_ITEMS_FIELD,
'problem_severity_level',
]}
deriveFieldValues={({ fieldPath, record: nextRecord }) => {
const fieldName = getFieldNameFromPath(fieldPath);
if (!PROBLEM_SEVERITY_SOURCE_FIELDS.includes(fieldName)) {
return {};
}
return {
'metadata.problem_severity_level': calculateProblemSeverityLevel(
nextRecord?.metadata,
),
};
}}
preferredViewName={activeGridView?.title || activeGridView?.name} preferredViewName={activeGridView?.title || activeGridView?.name}
authorizationView={activeGridView} authorizationView={activeGridView}
/> />
...@@ -1733,10 +1805,20 @@ export default function ProblemRectificationListPage() { ...@@ -1733,10 +1805,20 @@ export default function ProblemRectificationListPage() {
fieldDef={fieldDef} fieldDef={fieldDef}
value={formValues[fieldDef.name]} value={formValues[fieldDef.name]}
onChange={(nextValue) => onChange={(nextValue) =>
setFormValues((current) => ({ setFormValues((current) => {
...current, const nextValues = {
[fieldDef.name]: nextValue, ...current,
})) [fieldDef.name]: nextValue,
};
return PROBLEM_SEVERITY_SOURCE_FIELDS.includes(fieldDef.name)
? {
...nextValues,
problem_severity_level:
calculateProblemSeverityLevel(nextValues),
}
: nextValues;
})
} }
/> />
{FIELD_HINTS[fieldDef.name] ? ( {FIELD_HINTS[fieldDef.name] ? (
......
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { calculateProblemSeverityLevel } from './problemSeverity';
import { queryRecords } from '@/services/DataService'; import { queryRecords } from '@/services/DataService';
export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification'; export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification';
...@@ -455,7 +456,7 @@ function normalizeProblemRecord(record) { ...@@ -455,7 +456,7 @@ function normalizeProblemRecord(record) {
problemType: normalizeText(metadata.problem_type), problemType: normalizeText(metadata.problem_type),
relatedIssueDomain: normalizeText(metadata.related_issue_domain) || '未分类', relatedIssueDomain: normalizeText(metadata.related_issue_domain) || '未分类',
brief: normalizeText(metadata.problem_brief) || '暂无问题简述', brief: normalizeText(metadata.problem_brief) || '暂无问题简述',
severityLevel: normalizeText(metadata.problem_severity_level), severityLevel: calculateProblemSeverityLevel(metadata),
categoryLevel1: normalizeText(metadata.problem_category_level_1), categoryLevel1: normalizeText(metadata.problem_category_level_1),
categoryLevel2: normalizeText(metadata.problem_category_level_2), categoryLevel2: normalizeText(metadata.problem_category_level_2),
categoryLevel3: normalizeText(metadata.problem_category_level_3), categoryLevel3: normalizeText(metadata.problem_category_level_3),
......
...@@ -9,6 +9,7 @@ import { ...@@ -9,6 +9,7 @@ import {
calculateRectificationQualityRating, calculateRectificationQualityRating,
DEPARTMENT_GROUPS, DEPARTMENT_GROUPS,
} from './problemRectificationDashboardService'; } from './problemRectificationDashboardService';
import { calculateProblemSeverityLevel } from './problemSeverity';
import { import {
buildViewColumns, buildViewColumns,
checkViewCondition, checkViewCondition,
...@@ -48,7 +49,6 @@ const LEVEL_ORDER = ['高', '中', '低']; ...@@ -48,7 +49,6 @@ const LEVEL_ORDER = ['高', '中', '低'];
const FILTER_FIELD_PATHS = { 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',
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',
...@@ -189,7 +189,13 @@ export function getProblemRectificationFlowTableUrl(messageName) { ...@@ -189,7 +189,13 @@ 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(/\/+$/, '');
return `${basePath}${PROBLEM_RECTIFICATION_FLOW_TABLE_PATH}${query}`; const appPath = `${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) {
...@@ -694,6 +700,40 @@ function buildInspectionReportMonthFilter(month) { ...@@ -694,6 +700,40 @@ function buildInspectionReportMonthFilter(month) {
}; };
} }
function buildProblemSeverityFilter(severityLevel) {
if (!LEVEL_ORDER.includes(severityLevel)) {
return {};
}
const economicLossFilter = {
'metadata.has_economic_loss': { $in: AFFIRMATIVE_FILTER_VALUES },
};
const externalImpactFilter = {
'metadata.has_external_impact': { $in: AFFIRMATIVE_FILTER_VALUES },
};
if (severityLevel === '高') {
return { $or: [economicLossFilter, externalImpactFilter] };
}
const noHighSeverityCause = {
'metadata.has_economic_loss': { $nin: AFFIRMATIVE_FILTER_VALUES },
'metadata.has_external_impact': { $nin: AFFIRMATIVE_FILTER_VALUES },
};
if (severityLevel === '中') {
return {
...noHighSeverityCause,
'metadata.has_employee_accountability': { $in: AFFIRMATIVE_FILTER_VALUES },
};
}
return {
...noHighSeverityCause,
'metadata.has_employee_accountability': { $nin: AFFIRMATIVE_FILTER_VALUES },
};
}
function buildFocusFlagFilter(focusFlag) { function buildFocusFlagFilter(focusFlag) {
if (focusFlag === 'consequence') { if (focusFlag === 'consequence') {
return { return {
...@@ -706,7 +746,7 @@ function buildFocusFlagFilter(focusFlag) { ...@@ -706,7 +746,7 @@ function buildFocusFlagFilter(focusFlag) {
} }
if (focusFlag === 'high_severity') { if (focusFlag === 'high_severity') {
return { 'metadata.problem_severity_level': '高' }; return buildProblemSeverityFilter('高');
} }
if (focusFlag === 'low_rating') { if (focusFlag === 'low_rating') {
...@@ -828,6 +868,7 @@ export function buildProblemRectificationQueryFilter(filters = {}, gridView, sub ...@@ -828,6 +868,7 @@ export function buildProblemRectificationQueryFilter(filters = {}, gridView, sub
buildViewConditionsFilter(gridView?.settings?.filters), buildViewConditionsFilter(gridView?.settings?.filters),
...uiFieldClauses, ...uiFieldClauses,
buildInspectionReportMonthFilter(filters?.inspectionReportMonth), buildInspectionReportMonthFilter(filters?.inspectionReportMonth),
buildProblemSeverityFilter(filters?.severityLevel),
buildCalculatedQualityRatingFilter(filters?.overallRating), buildCalculatedQualityRatingFilter(filters?.overallRating),
buildFocusFlagFilter(filters?.focusFlag), buildFocusFlagFilter(filters?.focusFlag),
buildKeywordFilter(filters?.keyword, columns), buildKeywordFilter(filters?.keyword, columns),
...@@ -893,7 +934,7 @@ export function normalizeProblemRectificationRecord(record) { ...@@ -893,7 +934,7 @@ export function normalizeProblemRectificationRecord(record) {
problemFrom: normalizeText(metadata.problem_from), problemFrom: normalizeText(metadata.problem_from),
relatedIssueDomain: normalizeText(metadata.related_issue_domain), relatedIssueDomain: normalizeText(metadata.related_issue_domain),
brief: normalizeText(metadata.problem_brief), brief: normalizeText(metadata.problem_brief),
severityLevel: normalizeText(metadata.problem_severity_level), severityLevel: calculateProblemSeverityLevel(metadata),
categoryLevel1: normalizeText(metadata.problem_category_level_1), categoryLevel1: normalizeText(metadata.problem_category_level_1),
categoryLevel2: normalizeText(metadata.problem_category_level_2), categoryLevel2: normalizeText(metadata.problem_category_level_2),
categoryLevel3: normalizeText(metadata.problem_category_level_3), categoryLevel3: normalizeText(metadata.problem_category_level_3),
...@@ -1375,10 +1416,15 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) { ...@@ -1375,10 +1416,15 @@ export function buildProblemRectificationCreateFormFields(gridView, subject) {
} }
export function buildInitialCreateValues(createFields) { export function buildInitialCreateValues(createFields) {
return createFields.reduce((values, field) => ({ const values = createFields.reduce((nextValues, field) => ({
...values, ...nextValues,
[field.fieldDef.name]: emptyValueForField(field.fieldDef), [field.fieldDef.name]: emptyValueForField(field.fieldDef),
}), {}); }), {});
return {
...values,
problem_severity_level: calculateProblemSeverityLevel(values),
};
} }
export function buildProblemRectificationCreatePayload(formValues, createFields = []) { export function buildProblemRectificationCreatePayload(formValues, createFields = []) {
...@@ -1400,6 +1446,9 @@ export function buildProblemRectificationCreatePayload(formValues, createFields ...@@ -1400,6 +1446,9 @@ export function buildProblemRectificationCreatePayload(formValues, createFields
return { return {
subject: PROBLEM_RECTIFICATION_SUBJECT, subject: PROBLEM_RECTIFICATION_SUBJECT,
title: projectName || problemBrief || '问题整改记录', title: projectName || problemBrief || '问题整改记录',
metadata, metadata: {
...metadata,
problem_severity_level: calculateProblemSeverityLevel(metadata),
},
}; };
} }
import { normalizeText } from '@/utils/subjectView';
const AFFIRMATIVE_VALUES = new Set(['是', '有', '涉及', 'TRUE', '1', 'Y', 'YES']);
export const PROBLEM_SEVERITY_SOURCE_FIELDS = [
'has_economic_loss',
'has_external_impact',
'has_employee_accountability',
];
export function calculateProblemSeverityLevel(metadata = {}) {
const isAffirmative = (value) =>
AFFIRMATIVE_VALUES.has(normalizeText(value).toUpperCase());
if (
isAffirmative(metadata.has_economic_loss) ||
isAffirmative(metadata.has_external_impact)
) {
return '高';
}
if (isAffirmative(metadata.has_employee_accountability)) {
return '中';
}
return '低';
}
...@@ -155,6 +155,16 @@ type Record = { ...@@ -155,6 +155,16 @@ type Record = {
| 部门延期申请明细 | `problem_rectification_delay_request_items` | object | `[]` | 否 | 是 | 否 | 否 | 否 | | 部门延期申请明细 | `problem_rectification_delay_request_items` | object | `[]` | 否 | 是 | 否 | 否 | 否 |
| 状态锁定 | `is_locked` | boolean | `false` | 否 | 否 | 否 | 否 | 否 | | 状态锁定 | `is_locked` | boolean | `false` | 否 | 否 | 否 | 否 | 否 |
#### 问题综合严重程度计算规则
`problem_severity_level` 由系统根据三个问题后果字段自动计算,不由用户手工选择:
- `has_economic_loss` 或 `has_external_impact` 任一项为“是”时,结果为“高”
- 上述两项均不为“是”,仅 `has_employee_accountability` 为“是”时,结果为“中”
- 三项均不为“是”或未填写时,结果为“低”
创建记录和编辑上述任一来源字段时,计算结果写入 `metadata.problem_severity_level`。列表与驾驶舱读取历史记录时也按相同规则即时计算。
### 3.1.1 select 字段选项 ### 3.1.1 select 字段选项
- `inspect_the_implementing_unit`(检查实施单位):`法律合规部`、`源泰律师事务所(合规有效性评估)`、`普华永道会计师事务所`、`风险管理部`、`内部审计`、`内部合规检查`、`外部审计`、`外部检查`、`总行审计`、`工会经费审査`、`国家审计署`、`鑫沅资产`、`鑫元基金`、`其他` - `inspect_the_implementing_unit`(检查实施单位):`法律合规部`、`源泰律师事务所(合规有效性评估)`、`普华永道会计师事务所`、`风险管理部`、`内部审计`、`内部合规检查`、`外部审计`、`外部检查`、`总行审计`、`工会经费审査`、`国家审计署`、`鑫沅资产`、`鑫元基金`、`其他`
...@@ -177,7 +187,7 @@ type Record = { ...@@ -177,7 +187,7 @@ type Record = {
- 新增驾驶舱扩展字段当前在线上 Subject 中大多仍为 `text` 类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析 - 新增驾驶舱扩展字段当前在线上 Subject 中大多仍为 `text` 类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析
- 其余 `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` 平级;新发起延期默认按部门写入 `problem_rectification_delay_request_items` - `problem_rectification_delay_request` 为历史问题级单次延期申请嵌套对象,与 `department_rectification_items` 平级;新发起延期默认按部门写入 `problem_rectification_delay_request_items`
- `problem_rectification_delay_request_items` 为部门级延期申请明细数组;每个责任部门 1 条延期申请,独立保存填写人、一级审批人、最终审批人、申请完成时间、延期事由、状态和待办 ID - `problem_rectification_delay_request_items` 为部门级延期申请明细数组;每个责任部门 1 条延期申请,独立保存填写人、一级审批人、最终审批人、申请完成时间、延期事由、状态和待办 ID
- `is_locked` 为问题整改主记录的状态锁定标记;仅整改进度汇总值为 `已整改` 的记录允许写入 `true`,锁定后所有业务字段和流程状态均不可修改,只有解锁操作可以将其写回 `false` - `is_locked` 为问题整改主记录的状态锁定标记;仅整改进度汇总值为 `已整改` 的记录允许写入 `true`,锁定后所有业务字段和流程状态均不可修改,只有解锁操作可以将其写回 `false`
...@@ -370,13 +380,13 @@ type Record = { ...@@ -370,13 +380,13 @@ type Record = {
- `reference_record_id` 与 `restriction_value` 保持一致,作为显式的“关联记录 ID”冗余字段 - `reference_record_id` 与 `restriction_value` 保持一致,作为显式的“关联记录 ID”冗余字段
- `subject_name` 保存被推送记录所属对象名,当前整改流程固定为 `problem_rectification` - `subject_name` 保存被推送记录所属对象名,当前整改流程固定为 `problem_rectification`
- `flow_mode` 区分流程:`department` 表示单条问题按责任部门生成整改明细行,`batch` 表示多选问题的单填写人 / 单审批人简单推送,`delay` 表示历史问题级延期申请,`delay_department` 表示按责任部门生成延期申请明细 - `flow_mode` 区分流程:`department` 表示单条问题按责任部门生成整改明细行,`batch` 表示多选问题的单填写人 / 单审批人简单推送,`delay` 表示历史问题级延期申请,`delay_department` 表示按责任部门生成延期申请明细
- `department_item_ids` 保存本次部门流程涉及的部门级 `item_id`;整改流程对应 `department_rectification_items[].item_id`,延期流程对应 `problem_rectification_delay_request_items[].item_id` - `department_item_ids` 保存本次流程涉及的部门级 `item_id`;单条和多条整改流程对应 `department_rectification_items[].item_id`,延期流程对应 `problem_rectification_delay_request_items[].item_id`
- 部分线上 `message_reference_id` 对象会过滤未配置的 `flow_mode` 或 `department_item_ids`;延期流程页须回退按关联问题 `problem_rectification_delay_request_items[].flow_record_id._id` 匹配当前流程主单 `_id`,或按共享的 `progress_id_editor` 匹配,不能将其当作普通整改流程 - 部分线上 `message_reference_id` 对象会过滤未配置的 `flow_mode` 或 `department_item_ids`;延期流程页须回退按关联问题 `problem_rectification_delay_request_items[].flow_record_id._id` 匹配当前流程主单 `_id`,或按共享的 `progress_id_editor` 匹配,不能将其当作普通整改流程
- `progress_id_editor`、`progress_id_auditor`、`progress_id_sender` 用于记录每一步用户确认动作生成的流程 ID,用来区分当前流程推进到了哪一步 - 批量流程的填写、复核、最终审批状态及待办 ID 直接使用流程主单顶层的 `editor_status`、`auditor_status`、`sender_status` 和 `progress_id_*`,同一批次的全部问题共享流程状态
- `editor_status`:当前项目统一使用 `未完成`、`已完成` - `editor_status`:当前项目统一使用 `未完成`、`已完成`
- `auditor_status`:当前项目统一使用 `未完成`、`已完成` - `auditor_status`:当前项目统一使用 `未完成`、`已完成`
- `sender_status`:当前项目统一使用 `未完成`、`已完成` - `sender_status`:当前项目统一使用 `未完成`、`已完成`
- 当复核或最终审核驳回时,流程主单会回写新的 `progress_id_editor`,并将 `editor_status` 重置为 `未完成`,从而重新进入填写阶段 - 当批量流程复核或最终审核驳回时,流程主单 `editor_status` 重置为 `未完成` 并回写新的 `progress_id_editor`,同一批次的全部问题共同重新进入填写阶段
## 3.5 项目管理 ## 3.5 项目管理
......
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