Commit 004e66ec authored by David Yang's avatar David Yang

feat: 支持部门级延期申请

parent 3917767f
......@@ -466,6 +466,7 @@ export default function RecordWorkbench({
states = {},
renderCell,
renderExpandedRow,
getExpandedRecordRows,
isRowExpandable,
getRecordId = defaultGetRecordId,
getRecordLabel = defaultGetRecordLabel,
......@@ -534,7 +535,8 @@ export default function RecordWorkbench({
);
const hasColumnGroups = columnGroupMeta.groups.length > 0;
const showColumnGroupHeader = columnGroupSegments.some((segment) => segment.group);
const hasExpandableRows = typeof renderExpandedRow === 'function';
const hasExpandedRecordRows = typeof getExpandedRecordRows === 'function';
const hasExpandableRows = hasExpandedRecordRows || typeof renderExpandedRow === 'function';
const hasExpandColumn = hasExpandableRows && showExpandColumn;
const tableColSpan =
stickyColumns.orderedColumns.length +
......@@ -997,6 +999,21 @@ export default function RecordWorkbench({
openDetail,
};
const visibleRowActions = rowActions.filter((action) => !isActionHidden(action, rowContext));
const expandedRecordRows = (() => {
if (!expanded || !expandable || !hasExpandedRecordRows) {
return EMPTY_ARRAY;
}
const nextExpandedRecordRows = getExpandedRecordRows({
record,
rowContext,
columns: stickyColumns.orderedColumns,
});
return Array.isArray(nextExpandedRecordRows)
? nextExpandedRecordRows
: EMPTY_ARRAY;
})();
return (
<Fragment key={recordId}>
......@@ -1101,7 +1118,83 @@ export default function RecordWorkbench({
</TableCell>
) : null}
</TableRow>
{expanded && expandable ? (
{expanded && expandable && hasExpandedRecordRows ? (
expandedRecordRows.map((expandedRecord, expandedIndex) => {
const expandedRecordId =
expandedRecord?.key ||
expandedRecord?.id ||
expandedRecord?._id ||
`${recordId}-expanded-${expandedIndex}`;
return (
<TableRow
key={expandedRecordId}
className="align-top bg-rose-50/35"
>
{selectable ? (
<TableCell
className="align-top sticky left-0 z-30 bg-rose-50/95 shadow-[10px_0_20px_rgba(15,23,42,0.05)]"
style={{ width: SELECTION_COLUMN_WIDTH, minWidth: SELECTION_COLUMN_WIDTH }}
/>
) : null}
{hasExpandColumn ? (
<TableCell className="w-12 align-top bg-rose-50/70" style={{ width: 48, minWidth: 48 }} />
) : null}
{stickyColumns.orderedColumns.map((column) => {
const stickyMeta = stickyColumns.stickyMeta[column.field];
return (
<TableCell
key={column.field}
style={{
width: column.width || 180,
minWidth: Math.min(Math.max(column.width || 180, 150), 360),
left: stickyMeta?.side === 'left' ? `${stickyMeta.offset}px` : undefined,
right: stickyMeta?.side === 'right' ? `${stickyMeta.offset}px` : undefined,
}}
className={clsx(
'align-top',
getStickyCellClass(stickyMeta),
)}
>
{(() => {
if (column.render) {
return column.render(expandedRecord, column);
}
if (renderCell) {
const customNode = renderCell({
record: expandedRecord,
column,
expandable: false,
expanded: false,
toggleExpanded: () => {},
parentRecord: record,
isExpandedRecord: true,
});
if (customNode !== undefined) {
return customNode;
}
}
return defaultRenderCell(expandedRecord, column);
})()}
</TableCell>
);
})}
{visibleRowActions.length > 0 ? (
<TableCell
className="align-top sticky right-0 z-30 bg-rose-50/95 shadow-[-10px_0_20px_rgba(15,23,42,0.05)]"
style={{ width: actionColumnWidth, minWidth: actionColumnWidth }}
/>
) : null}
</TableRow>
);
})
) : null}
{expanded && expandable && !hasExpandedRecordRows ? (
<TableRow key={`${recordId}-expanded`} className="bg-zinc-50/70">
<TableCell colSpan={tableColSpan} className="px-6 py-4">
{renderExpandedRow({ record, rowContext })}
......
......@@ -34,6 +34,7 @@ import {
AUDIT_STATUS_PENDING,
AUDIT_STATUS_REJECTED,
DELAY_REQUEST_FIELD,
DELAY_REQUEST_ITEMS_FIELD,
EDITOR_STATUS_COMPLETED,
EDITOR_STATUS_PENDING,
FLOW_MODE_BATCH,
......@@ -41,9 +42,11 @@ import {
FLOW_MODE_DELAY_DEPARTMENT,
FLOW_MODE_DEPARTMENT,
PROBLEM_RECTIFICATION_SUBJECT,
buildDepartmentParentMetadataPatch,
buildDepartmentParentBatchUpdateData,
getProblemRectificationFlowTableUrl,
normalizeDelayRequest,
normalizeDelayRequestItems,
normalizeDepartmentRectificationItems,
} from '../problemRectificationListService';
......@@ -54,6 +57,7 @@ const FLOW_MANAGED_PARENT_FIELDS = [
'final_audit_status',
'department_rectification_items',
DELAY_REQUEST_FIELD,
DELAY_REQUEST_ITEMS_FIELD,
];
const DEPARTMENT_MANAGED_PARENT_FIELDS = [
...FLOW_MANAGED_PARENT_FIELDS,
......@@ -248,12 +252,50 @@ function buildBatchRows(records = [], metadata = {}) {
});
}
function buildDelayRows(records = [], metadata = {}) {
function buildDelayRows(records = [], metadata = {}, flowMode = FLOW_MODE_DELAY) {
const editor = toId(toArray(metadata.editor)[0]);
const auditor = toId(toArray(metadata.auditor)[0]);
const scopedItemIds = new Set(toArray(metadata.department_item_ids).map(toId).filter(Boolean));
return records.flatMap((record) => {
const recordMetadata = record.metadata || {};
if (flowMode === FLOW_MODE_DELAY_DEPARTMENT) {
return normalizeDelayRequestItems(recordMetadata[DELAY_REQUEST_ITEMS_FIELD])
.filter(
(request) =>
scopedItemIds.size === 0 ||
scopedItemIds.has(request.item_id) ||
scopedItemIds.has(request.source_department_item_id),
)
.map((request) => ({
key: `${record._id || record.name}-delay-${request.item_id}`,
flowMode: FLOW_MODE_DELAY_DEPARTMENT,
record,
item: request,
itemId: request.item_id,
sourceDepartmentItemId: request.source_department_item_id || request.item_id,
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: request.responsible_department || '-',
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,
}));
}
const request = normalizeDelayRequest(recordMetadata[DELAY_REQUEST_FIELD]);
if (!request) {
return [];
......@@ -308,7 +350,7 @@ export default function ProblemRectificationFlowTablePage() {
const rows = useMemo(
() => {
if (flowMode === FLOW_MODE_DELAY || flowMode === FLOW_MODE_DELAY_DEPARTMENT) {
return buildDelayRows(records, metadata);
return buildDelayRows(records, metadata, flowMode);
}
return flowMode === FLOW_MODE_DEPARTMENT
? buildDepartmentRows(records)
......@@ -453,6 +495,67 @@ export default function ProblemRectificationFlowTablePage() {
throw new Error('当前问题整改记录已锁定,不允许继续修改或审批。');
}
if (row.flowMode === FLOW_MODE_DELAY_DEPARTMENT) {
const currentRequests = normalizeDelayRequestItems(
latestRecord?.metadata?.[DELAY_REQUEST_ITEMS_FIELD],
);
const currentRequest = currentRequests.find(
(request) =>
request.item_id === row.itemId ||
request.source_department_item_id === row.sourceDepartmentItemId,
);
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('当前部门延期申请已关联其他流程,请刷新后重试。');
}
const nextRequests = currentRequests.map((request) =>
request.item_id === currentRequest.item_id
? {
...request,
...requestPatch,
}
: request,
);
const metadataPatch = {
[DELAY_REQUEST_ITEMS_FIELD]: nextRequests,
};
if (recordPatch.the_time_of_rectification) {
const currentDepartmentItems = normalizeDepartmentRectificationItems(latestRecord);
const sourceDepartmentItemId = row.sourceDepartmentItemId || row.itemId;
if (currentDepartmentItems.length > 0) {
const nextDepartmentItems = currentDepartmentItems.map((item) =>
item.item_id === sourceDepartmentItemId
? {
...item,
the_time_of_rectification: recordPatch.the_time_of_rectification,
updated_by: currentUserId,
updated_at: dayjs().toISOString(),
}
: item,
);
Object.assign(metadataPatch, buildDepartmentParentMetadataPatch(nextDepartmentItems));
} else {
metadataPatch.the_time_of_rectification = recordPatch.the_time_of_rectification;
}
}
await updateRecord(row.recordId, {
metadata: metadataPatch,
});
return;
}
const currentRequest = normalizeDelayRequest(
latestRecord?.metadata?.[DELAY_REQUEST_FIELD],
);
......
......@@ -34,7 +34,7 @@ import {
} from '@/services/DataService';
import * as Headless from '@headlessui/react';
import { ChevronRightIcon } from '@heroicons/react/20/solid';
import { LockClosedIcon, LockOpenIcon } from '@heroicons/react/24/outline';
import { LockClosedIcon, LockOpenIcon, TrashIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import dayjs from 'dayjs';
import { useCallback, useEffect, useMemo, useState } from 'react';
......@@ -50,13 +50,15 @@ import {
PROBLEM_RECTIFICATION_SUBJECT,
AUDIT_STATUS_PENDING,
DELAY_REQUEST_FIELD,
DELAY_REQUEST_ITEMS_FIELD,
DEPARTMENT_RECTIFICATION_FIELD,
EDITOR_STATUS_PENDING,
FLOW_MODE_BATCH,
FLOW_MODE_DELAY,
FLOW_MODE_DELAY_DEPARTMENT,
FLOW_MODE_DEPARTMENT,
PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS,
buildDepartmentAssignmentsFromRecord,
buildDepartmentDelayRequestItems,
buildDepartmentParentBatchUpdateData,
buildDepartmentRectificationItems,
buildGridViews,
......@@ -70,6 +72,7 @@ import {
loadProblemRectificationListResources,
loadProblemRectificationViewCountMap,
normalizeDelayRequest,
normalizeDelayRequestItems,
normalizeDepartmentRectificationItems,
} from '../problemRectificationListService';
......@@ -470,6 +473,17 @@ function FieldValuePreview({ column, record }) {
const value = getFieldValue(rawRecord, column.field);
if (fieldName === 'title') {
if (record.isDepartmentDetailRow) {
return (
<div className="flex flex-wrap items-center gap-2">
<span className="inline-flex rounded-full bg-rose-50 px-2.5 py-1 text-xs font-medium text-rose-700 ring-1 ring-rose-100">
明细
</span>
<span className="font-medium text-slate-800">{record.title || '部门整改明细'}</span>
</div>
);
}
return <div className="font-medium text-slate-900">{record.title || '(无标题)'}</div>;
}
......@@ -559,13 +573,52 @@ function FieldValuePreview({ column, record }) {
);
}
function DepartmentDetailField({ label, children }) {
return (
<div className="min-w-0">
<div className="text-xs font-medium text-slate-400">{label}</div>
<div className="mt-1.5 break-words text-sm text-slate-800">{children || '-'}</div>
</div>
);
function buildUserDisplayValue(userId, userLabels = {}) {
if (!userId) {
return [];
}
return [
{
_id: userId,
display_name: userLabels[userId] || userId,
},
];
}
function buildDepartmentDetailRecord(parentRecord, item, userLabels = {}) {
const parentRaw = parentRecord.raw || parentRecord;
const parentMetadata = parentRaw.metadata || {};
const departmentName = item.responsible_department || '未指定责任部门';
const metadata = {
...parentMetadata,
responsible_department: [departmentName],
fill_in_the_people: buildUserDisplayValue(item.editor, userLabels),
review_people: buildUserDisplayValue(item.auditor, userLabels),
rectification_progress: item.rectification_progress || '',
rectification_situation: item.rectification_situation || '',
the_time_of_rectification: item.the_time_of_rectification || null,
rectification_proof_materials: item.rectification_proof_materials || null,
audit_status: item.audit_status || AUDIT_STATUS_PENDING,
final_audit_status: item.final_audit_status || AUDIT_STATUS_PENDING,
};
return {
...parentRecord,
id: `${parentRecord.id || parentRecord.name || parentRaw._id || parentRaw.name}-${item.item_id}`,
key: `${parentRecord.id || parentRecord.name || parentRaw._id || parentRaw.name}-${item.item_id}`,
title: `${departmentName} 部门明细`,
fillUsers: [userLabels[item.editor] || item.editor].filter(Boolean),
reviewUsers: [userLabels[item.auditor] || item.auditor].filter(Boolean),
departmentItems: [],
hasDepartmentItems: false,
isDepartmentDetailRow: true,
parentRecord,
raw: {
...parentRaw,
metadata,
},
};
}
export default function ProblemRectificationListPage() {
......@@ -598,8 +651,7 @@ export default function ProblemRectificationListPage() {
const [fillerUsers, setFillerUsers] = useState([]);
const [auditorUsers, setAuditorUsers] = useState([]);
const [departmentAssignments, setDepartmentAssignments] = useState([]);
const [delayEditorUsers, setDelayEditorUsers] = useState([]);
const [delayAuditorUsers, setDelayAuditorUsers] = useState([]);
const [delayDepartmentAssignments, setDelayDepartmentAssignments] = useState([]);
const [successMessage, setSuccessMessage] = useState('');
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE);
......@@ -986,10 +1038,30 @@ export default function ProblemRectificationListPage() {
);
};
const updateDelayDepartmentAssignment = (itemId, patch) => {
setDelayDepartmentAssignments((current) =>
current.map((item) =>
item.item_id === itemId
? {
...item,
...patch,
}
: item,
),
);
};
const removeDepartmentAssignment = (itemId) => {
setDepartmentAssignments((current) => current.filter((item) => item.item_id !== itemId));
};
const removeDelayDepartmentAssignment = (itemId) => {
setDelayDepartmentAssignments((current) => current.filter((item) => item.item_id !== itemId));
};
const closeDelayPushModal = () => {
setShowDelayPushModal(false);
setDelayEditorUsers([]);
setDelayAuditorUsers([]);
setDelayDepartmentAssignments([]);
};
const openDelayPushModal = () => {
......@@ -1004,26 +1076,32 @@ export default function ProblemRectificationListPage() {
return;
}
if (record.delayRequest) {
if (record.delayRequest || record.delayRequestItems?.length > 0) {
setError('当前问题已发起延期申请,不能重复推送。');
return;
}
setError('');
setDelayEditorUsers([]);
setDelayAuditorUsers([]);
setDelayDepartmentAssignments(buildDepartmentAssignmentsFromRecord(record.raw || record));
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 个一级审批人。');
if (delayDepartmentAssignments.length === 0) {
setError('请至少保留 1 个责任部门配置后再推送延期申请。');
return;
}
const hasMissingDepartmentUser = delayDepartmentAssignments.some(
(item) => !item.editor?._id || !item.auditor?._id,
);
if (hasMissingDepartmentUser) {
setError('请为每个责任部门选择 1 个延期申请填写人和 1 个一级审批人。');
return;
}
......@@ -1043,26 +1121,33 @@ export default function ProblemRectificationListPage() {
if (latestRecord?.metadata?.is_locked === true) {
throw new Error('当前问题整改记录已锁定,不允许发起延期申请。');
}
if (normalizeDelayRequest(latestRecord?.metadata?.[DELAY_REQUEST_FIELD])) {
if (
normalizeDelayRequest(latestRecord?.metadata?.[DELAY_REQUEST_FIELD]) ||
normalizeDelayRequestItems(latestRecord?.metadata?.[DELAY_REQUEST_ITEMS_FIELD]).length > 0
) {
throw new Error('当前问题已发起延期申请,不能重复推送。');
}
const editorId = editorProfile._id;
const auditorId = auditorProfile._id;
const editorIds = Array.from(
new Set(delayDepartmentAssignments.map((item) => item.editor?._id).filter(Boolean)),
);
const auditorIds = Array.from(
new Set(delayDepartmentAssignments.map((item) => item.auditor?._id).filter(Boolean)),
);
const messageRecord = await createRecord({
subject: 'message_reference_id',
title: `推送延期申请填写 ${dayjs().format('YYYY-MM-DD HH:mm')}`,
metadata: {
sender: currentUserId,
editor: [editorId],
auditor: [auditorId],
editor: editorIds,
auditor: auditorIds,
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: [],
flow_mode: FLOW_MODE_DELAY_DEPARTMENT,
department_item_ids: delayDepartmentAssignments.map((item) => item.item_id),
editor_status: EDITOR_STATUS_PENDING,
auditor_status: EDITOR_STATUS_PENDING,
sender_status: EDITOR_STATUS_PENDING,
......@@ -1071,50 +1156,49 @@ export default function ProblemRectificationListPage() {
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 progressIdEditorByItemId = {};
for (const assignment of delayDepartmentAssignments) {
const editorProfile = assignment.editor?._id ? await loadUser(assignment.editor._id) : assignment.editor;
const oaProcessResult = await executeAction('oa_notice:createProgress', {
title: `${dayjs().format('YYYY-MM-DD')} 填写延期申请 - ${assignment.responsible_department}`,
classify: '流程待办',
contentType: '流程待办',
receiveUserId: editorProfile?._id || assignment.editor?._id,
receiveUserName: editorProfile?.username || assignment.editor?.username || assignment.editor?._id,
receiveDisplayName:
editorProfile?.display_name ||
assignment.editor?.display_name ||
assignment.editor?.username ||
assignment.editor?._id,
sendUserId: currentUserId,
sendUserName: currentUserInfo?.username || '',
sendDisplayName: currentUserInfo?.display_name || '',
returnUrl,
});
progressIdEditorByItemId[assignment.item_id] = oaProcessResult?.data?.taskId || '';
}
const delayRequestItems = buildDepartmentDelayRequestItems({
record: latestRecord,
assignments: delayDepartmentAssignments,
currentUser: currentUserInfo,
flowRecordId,
progressIdEditorByItemId,
});
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 },
metadata: { [DELAY_REQUEST_ITEMS_FIELD]: delayRequestItems },
});
await updateRecord(messageRecord._id || messageRecord.name, {
metadata: { progress_id_editor: [progressIdEditor] },
metadata: { progress_id_editor: Object.values(progressIdEditorByItemId).filter(Boolean) },
});
const savedRecord = await loadRecord(selectedRecord.id);
const savedDelayRequest = normalizeDelayRequest(
savedRecord?.metadata?.[DELAY_REQUEST_FIELD],
const savedDelayRequestItems = normalizeDelayRequestItems(
savedRecord?.metadata?.[DELAY_REQUEST_ITEMS_FIELD],
);
if (savedDelayRequest?.flow_record_id !== flowRecordId) {
if (savedDelayRequestItems.length !== delayRequestItems.length) {
throw new Error('延期申请写入失败,请刷新后重试。');
}
......@@ -1123,7 +1207,7 @@ export default function ProblemRectificationListPage() {
content: `请点击链接填写延期申请:${returnUrl}`,
type: 'internal',
from: currentUserId,
to: [editorId],
to: editorIds,
settings: {},
});
await sendNotice(noticeResult.name);
......@@ -1131,7 +1215,7 @@ export default function ProblemRectificationListPage() {
closeDelayPushModal();
setSelectedIds([]);
await reloadList({ keepSuccessMessage: true });
setSuccessMessage('延期申请已推送,当前问题已进入填写流程。');
setSuccessMessage(`延期申请已按部门推送,已生成 ${delayRequestItems.length} 条部门延期申请。`);
} catch (requestError) {
setError(requestError?.message || '推送延期申请失败,请稍后重试。');
} finally {
......@@ -1155,11 +1239,16 @@ export default function ProblemRectificationListPage() {
}
if (isDepartmentFlow) {
if (departmentAssignments.length === 0) {
setError('请至少保留 1 个责任部门配置后再推送。');
return;
}
const hasMissingDepartmentUser = departmentAssignments.some(
(item) => !item.editor?._id || !item.auditor?._id,
);
if (departmentAssignments.length === 0 || hasMissingDepartmentUser) {
if (hasMissingDepartmentUser) {
setError('请为每个责任部门选择 1 个填报人和 1 个复核人。');
return;
}
......@@ -1429,66 +1518,10 @@ export default function ProblemRectificationListPage() {
selectable={quickSelectEnabled}
showExpandColumn={false}
isRowExpandable={(record) => record.departmentItems?.length > 0}
renderExpandedRow={({ record }) => (
<div className="sticky left-4 w-[calc(100vw-8rem)] max-w-[1320px] overflow-hidden border-y border-rose-100 bg-rose-50/25">
<div className="flex items-center justify-between gap-4 border-b border-rose-100 px-5 py-3">
<div className="text-sm font-semibold text-slate-900">部门整改明细</div>
<div className="text-xs text-slate-500">共 {record.departmentItems.length} 条</div>
</div>
<div className="divide-y divide-rose-100/80">
{(record.departmentItems || []).map((item) => (
<div
key={item.item_id}
className="grid gap-x-5 gap-y-4 px-5 py-4 sm:grid-cols-2 lg:grid-cols-4 2xl:grid-cols-8"
>
<DepartmentDetailField label="责任部门">
{item.responsible_department || '-'}
</DepartmentDetailField>
<DepartmentDetailField label="填写人">
{departmentUserLabels[item.editor] || item.editor || '-'}
</DepartmentDetailField>
<DepartmentDetailField label="审批人">
{departmentUserLabels[item.auditor] || item.auditor || '-'}
</DepartmentDetailField>
<DepartmentDetailField label="整改进度">
<StatusPill
text={item.rectification_progress || '未填写'}
tone={resolveProgressTone(item.rectification_progress)}
/>
</DepartmentDetailField>
<DepartmentDetailField label="预计完成时间">
{item.the_time_of_rectification
? dayjs(item.the_time_of_rectification).format('YYYY-MM-DD')
: '-'}
</DepartmentDetailField>
<DepartmentDetailField label="审批状态">
<StatusPill
text={item.audit_status || '未审批'}
tone={resolveAuditTone(item.audit_status)}
/>
</DepartmentDetailField>
<DepartmentDetailField label="最终审批">
<StatusPill
text={item.final_audit_status || '未审批'}
tone={resolveAuditTone(item.final_audit_status)}
/>
</DepartmentDetailField>
<DepartmentDetailField label="整改附件">
{item.rectification_proof_materials?.url ? (
<a
href={item.rectification_proof_materials.url}
target="_blank"
rel="noreferrer"
className="font-medium text-sky-700 hover:text-sky-900"
>
{item.rectification_proof_materials.name || '查看附件'}
</a>
) : '未上传'}
</DepartmentDetailField>
</div>
))}
</div>
</div>
getExpandedRecordRows={({ record }) => (
(record.departmentItems || []).map((item) =>
buildDepartmentDetailRecord(record, item, departmentUserLabels),
)
)}
toolbarActions={[
{
......@@ -1585,7 +1618,7 @@ export default function ProblemRectificationListPage() {
recordName={recordName}
onClose={onClose}
onDataChange={onDataChange}
disabledFields={[DELAY_REQUEST_FIELD]}
disabledFields={[DELAY_REQUEST_FIELD, DELAY_REQUEST_ITEMS_FIELD]}
preferredViewName={activeGridView?.title || activeGridView?.name}
/>
),
......@@ -1627,10 +1660,10 @@ export default function ProblemRectificationListPage() {
return (
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
{fieldPreview}
{record.isLocked ? (
{!record.isDepartmentDetailRow && record.isLocked ? (
<StatusPill text="已锁定" tone="amber" />
) : null}
{expandable ? (
{!record.isDepartmentDetailRow && expandable ? (
<Button
plain
className="text-rose-700"
......@@ -1724,13 +1757,18 @@ export default function ProblemRectificationListPage() {
</DialogDescription>
<DialogBody>
{isDepartmentPushMode ? (
<div className="overflow-hidden rounded-[24px] border border-zinc-200 bg-white">
<div className="space-y-3">
<div className="text-sm text-slate-600">
当前保留 {departmentAssignments.length} 条部门配置;可删除无需本次推送的部门,至少保留 1 条。
</div>
<div className="overflow-hidden rounded-[24px] border border-zinc-200 bg-white">
<Table bleed dense className="[--gutter:--spacing(4)]">
<TableHead>
<TableRow className="bg-zinc-50">
<TableHeader>责任部门</TableHeader>
<TableHeader>填报人</TableHeader>
<TableHeader>复核人</TableHeader>
<TableHeader className="w-20 text-right">操作</TableHeader>
</TableRow>
</TableHead>
<TableBody>
......@@ -1766,10 +1804,23 @@ export default function ProblemRectificationListPage() {
placeholder="搜索复核人"
/>
</TableCell>
<TableCell className="w-20 align-top text-right">
<button
type="button"
onClick={() => removeDepartmentAssignment(assignment.item_id)}
disabled={actionLoading}
className="inline-flex size-9 items-center justify-center rounded-lg text-slate-400 transition hover:bg-rose-50 hover:text-rose-600 disabled:cursor-not-allowed disabled:opacity-50"
aria-label={`删除 ${assignment.responsible_department} 部门配置`}
title="删除"
>
<TrashIcon className="size-4" aria-hidden="true" />
</button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
) : (
<div className="grid gap-6 md:grid-cols-2">
......@@ -1824,10 +1875,10 @@ export default function ProblemRectificationListPage() {
) : null}
{showDelayPushModal ? (
<Dialog open={showDelayPushModal} onClose={closeDelayPushModal} size="2xl">
<Dialog open={showDelayPushModal} onClose={closeDelayPushModal} size="5xl">
<DialogTitle>推送延期申请</DialogTitle>
<DialogDescription>
填写人将在流程页补充申请完成时间和延期事由,最终审批通过前不会修改当前预计完成时间。
按责任部门生成延期申请,填写人将在流程页补充申请完成时间和延期事由,最终审批通过前不会修改当前预计完成时间。
</DialogDescription>
<DialogBody>
<div className="rounded-lg border border-zinc-200 bg-zinc-50/80 p-4">
......@@ -1842,21 +1893,71 @@ export default function ProblemRectificationListPage() {
) || '未设置'}
</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 className="mt-6 space-y-3">
<div className="text-sm text-slate-600">
当前保留 {delayDepartmentAssignments.length} 条部门配置;可删除无需本次推送延期申请的部门,至少保留 1 条。
</div>
<div className="overflow-hidden rounded-[24px] border border-zinc-200 bg-white">
<Table bleed dense className="[--gutter:--spacing(4)]">
<TableHead>
<TableRow className="bg-zinc-50">
<TableHeader>责任部门</TableHeader>
<TableHeader>填写人</TableHeader>
<TableHeader>一级审批人</TableHeader>
<TableHeader className="w-20 text-right">操作</TableHeader>
</TableRow>
</TableHead>
<TableBody>
{delayDepartmentAssignments.map((assignment) => (
<TableRow key={assignment.item_id}>
<TableCell className="w-56 align-top">
<div className="font-medium text-slate-900">
{assignment.responsible_department}
</div>
</TableCell>
<TableCell className="align-top">
<RemoteUserSelect
label="填写人"
value={assignment.editor ? [assignment.editor] : []}
onChange={(users) =>
updateDelayDepartmentAssignment(assignment.item_id, {
editor: users[users.length - 1] || null,
})
}
onSearch={handleSearchUsers}
placeholder="搜索延期申请填写人"
/>
</TableCell>
<TableCell className="align-top">
<RemoteUserSelect
label="一级审批人"
value={assignment.auditor ? [assignment.auditor] : []}
onChange={(users) =>
updateDelayDepartmentAssignment(assignment.item_id, {
auditor: users[users.length - 1] || null,
})
}
onSearch={handleSearchUsers}
placeholder="搜索延期申请一级审批人"
/>
</TableCell>
<TableCell className="w-20 align-top text-right">
<button
type="button"
onClick={() => removeDelayDepartmentAssignment(assignment.item_id)}
disabled={actionLoading}
className="inline-flex size-9 items-center justify-center rounded-lg text-slate-400 transition hover:bg-rose-50 hover:text-rose-600 disabled:cursor-not-allowed disabled:opacity-50"
aria-label={`删除 ${assignment.responsible_department} 延期申请部门配置`}
title="删除"
>
<TrashIcon className="size-4" aria-hidden="true" />
</button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</DialogBody>
<DialogActions>
......@@ -1865,11 +1966,7 @@ export default function ProblemRectificationListPage() {
</Button>
<Button
color="blue"
disabled={
actionLoading ||
delayEditorUsers.length !== 1 ||
delayAuditorUsers.length !== 1
}
disabled={actionLoading || delayDepartmentAssignments.length === 0}
onClick={handleDelayPush}
>
{actionLoading ? '推送中...' : '确认推送'}
......
......@@ -188,9 +188,9 @@
### 5.5 推送延期申请
- 每次只允许选择 1 条问题记录,不要求该问题已生成 `department_rectification_items`
- 弹窗为本次问题级延期申请选择 1 个填写人和 1 个一级审批人,发起人为最终审批人
- 延期申请保存于 `metadata.problem_rectification_delay_request`,与 `department_rectification_items` 平级;每条问题最多保存 1 份延期申请
- 每次只允许选择 1 条问题记录,按该问题责任部门生成部门延期申请配置行;已有 `department_rectification_items` 时优先沿用其部门明细 ID
- 弹窗为每个保留的责任部门选择 1 个填写人和 1 个一级审批人,发起人为最终审批人
- 延期申请保存于 `metadata.problem_rectification_delay_request_items`,与 `department_rectification_items` 平级;历史 `metadata.problem_rectification_delay_request` 仅用于兼容旧问题级延期;每条问题最多保存 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 并重定向到上述表格处理页
- 已锁定、未选择流程人员或已有延期申请的问题不能发起
......@@ -300,7 +300,7 @@
1. 在问题整改列表中勾选一条或多条问题
2. 点击“推送问题整改表”
3. 如果只选择 1 条问题,系统按该问题的 `metadata.responsible_department` 生成部门配置行;每个责任部门必须选择 1 个填写人和 1 个审批人
3. 如果只选择 1 条问题,系统按该问题的 `metadata.responsible_department` 生成部门配置行;用户可删除本次无需推送的部门配置行,但至少需保留 1 行;保留的每个责任部门必须选择 1 个填写人和 1 个审批人
4. 如果选择多条问题,不拆分责任部门;本次推送只允许选择 1 个填写人和 1 个审批人,并批量应用到选中问题
5. 创建 `message_reference_id`(信息关联id)流程主单,写入:
- `sender`
......@@ -325,7 +325,8 @@
- 顶部批量操作栏出现“推送整改表”
- 点击后打开推送弹窗;单条问题展示部门配置表格,多选问题展示单填写人 / 单复核人配置
- 弹窗底部明确展示本次选中记录数量和生成的部门明细数量
- 弹窗中部门配置表展示自动生成的部门行数量,并提供行级删除操作;确认推送时只按保留的部门行生成 `department_rectification_items`、`department_item_ids`、OA 待办和内部通知
- 弹窗底部明确展示本次选中记录数量和最终生成的部门明细数量
- 成功后列表即时刷新,并给出已推送结果提示
### 7.3 填报人补录整改内容
......@@ -399,31 +400,32 @@
#### 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`(延期事由);草稿可以暂存,提交时两项均必填
2. 系统展示当前问题及主记录预计完成时间,并按责任部门生成延期申请配置行;已有 `department_rectification_items` 时优先使用其部门和 `item_id`,否则按 `metadata.responsible_department` 生成临时部门行
3. 发起人可删除本次不需要推送延期申请的部门配置行,但至少需保留 1 行;保留的每个部门必须选择 1 个填写人和 1 个一级审批人
4. 问题主记录已有 `problem_rectification_delay_request` 或 `problem_rectification_delay_request_items` 时不能重复推送
5. 确认推送后创建 `message_reference_id`,写入 `flow_mode = "delay_department"`、问题 ID、部门延期申请 `item_id` 列表、填写人、一级审批人和推送人
6. 在 `metadata.problem_rectification_delay_request_items` 初始化部门延期申请数组:原预计完成时间优先取对应部门明细当前 `the_time_of_rectification`,没有部门明细时取主记录当前 `the_time_of_rectification`;填写、一级审批、最终审批状态分别初始化为“未完成 / 未审批 / 未审批”
7. 系统为每个部门填写人创建 OA 待办并发送内部通知,链接使用相对地址 `/problem_rectification_new/problem_rectification_flow_table?id=<message_record_name>`
8. 填写人进入流程页后按部门填写 `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 待办
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. 驳回后填写人修改并重新提交时,流程再次经过一级审批和最终审批
1. 推送发起人作为最终审批人,按部门处理已通过一级审批的延期申请
2. 部门最终审批通过时写入该部门 `sender_status = "已审批"`、`sender_decided_at`,再把该部门 `requested_due_date` 写回对应 `department_rectification_items[].the_time_of_rectification`;如果不存在对应部门整改明细,则回退写回问题主记录 `the_time_of_rectification`
3. 部门延期申请不修改整改进度、整改情况、证明材料或整改审批状态
4. 部门最终审批驳回时写入该部门 `sender_status = "已驳回"`,把该部门 `editor_status` 重置为“未完成”,重新给该部门填写人创建 OA 待办;原预计完成时间保持不变
5. 驳回后部门填写人修改并重新提交时,该部门流程再次经过一级审批和最终审批
限制与异常处理:
- `metadata.is_locked = true` 时,禁止发起、保存、提交或审批延期申请
- 延期申请的状态和待办 ID 只写入问题主记录的嵌套对象,不覆盖部门明细的审批状态和原整改待办 ID
- 延期申请的状态和待办 ID 只写入 `problem_rectification_delay_request_items` 对应部门行,不覆盖部门整改明细的审批状态和原整改待办 ID
- 后台问题记录在提交前发生变化时,操作失败并提示刷新后重试
- OA 待办创建、业务记录写入或内部通知发送失败时展示错误,不显示成功提示
......@@ -455,8 +457,8 @@
- `sender` 记录推送发起人
- `editor` 记录填写人列表
- `auditor` 记录审核人列表
- `flow_mode` 记录流程模式:`department` 表示单条问题按责任部门拆分整改明细,`batch` 表示多选问题的单填写人 / 单审批人简单推送,`delay` 表示问题级延期申请;历史 `delay_department` 仅保留页面读取兼容
- `department_item_ids` 记录本次部门流程关联的 `department_rectification_items[].item_id`
- `flow_mode` 记录流程模式:`department` 表示单条问题按责任部门拆分整改明细,`batch` 表示多选问题的单填写人 / 单审批人简单推送,`delay` 表示历史问题级延期申请,`delay_department` 表示按责任部门拆分延期申请
- `department_item_ids` 记录本次部门流程关联的部门级 `item_id`;整改流程对应 `department_rectification_items[].item_id`,延期流程对应 `problem_rectification_delay_request_items[].item_id`
- `restriction_key` 默认固定为 `_id`
- `restriction_value` 保存本次推送绑定的整改记录 `_id[]`
- `reference_record_id` 作为关联记录 ID 冗余字段,和 `restriction_value` 一致
......@@ -473,20 +475,20 @@
`department_rectification_items` 存放在父问题 `metadata` 中:
- 问题整改主列表父行仍显示一条问题
- 父行的整改进度、审批状态、最终审批状态按部门明细汇总;全部一致则显示该状态,不一致则显示“部分整改”或“部分审批”
- 父行的整改进度、审批状态、最终审批状态按部门明细中最落后的状态汇总;例如 1 条“整改中”与 1 条“已整改”汇总为“整改中”,1 条“未审批”与 1 条“已审批”汇总为“未审批”
- 父行可展开显示部门明细,明细中分开展示责任部门、填写人、审批人、整改进度、预计完成时间、审批状态、最终审批状态和附件状态
- 存在部门明细的父行在标题右侧显示“N 条部门明细”入口;单部门推送同样生成 1 条明细。展开区使用贴合当前视口的响应式扁平信息网格,不嵌套横向子表格,并将填写人、审批人 ID 转为用户显示名
- 存在部门明细的父行在标题右侧显示“N 条部门明细”入口;单部门推送同样生成 1 条明细。展开后部门明细作为父行下方子行平铺展示,复用主表列顺序、列宽、固定列和列分组,与问题整改列头一一对齐;子行不参与主表勾选和批量操作,并将填写人、审批人 ID 转为用户显示名
- 转发后的整改填写、审批和最终审批均优先更新部门明细,父字段仅做兼容汇总展示
- 新流程页中,填写人只能维护本人负责行的整改业务字段,送审后锁定,复核驳回后恢复;复核驳回状态下对应审批人可以维护该行整改业务字段,审批通过后锁定,且审批人不能点击填写人的“送审”;流程推送人可以维护本次推送关联的父问题记录和全部部门明细整改业务字段,但不能直接编辑流程状态与待办 ID
- 推送人的内容修改保持当前审批状态不变,不触发重新送审
### 8.4 延期申请状态约定
- 每条问题主记录最多保存 1 个 `problem_rectification_delay_request` 嵌套对象,路径为 `metadata.problem_rectification_delay_request`
- 新延期流程按部门保存 `problem_rectification_delay_request_items` 数组,路径为 `metadata.problem_rectification_delay_request_items[]`;历史问题级延期对象 `metadata.problem_rectification_delay_request` 继续兼容读取
- `editor_status` 使用“未完成 / 已完成”;`auditor_status`、`sender_status` 使用“未审批 / 已审批 / 已驳回”
- `progress_id_editor`、`progress_id_auditor`、`progress_id_sender` 记录延期申请各阶段 OA 待办 ID
- `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`,嵌套对象继续保留完整审批记录
- 最终审批通过后,以该部门 `requested_due_date` 更新对应部门明细预计完成时间;无对应部门明细时才回退更新问题主记录 `the_time_of_rectification`,延期申请明细继续保留完整审批记录
### 8.5 主记录状态锁定约定
......
......@@ -29,6 +29,7 @@ export const PROBLEM_RECTIFICATION_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
export const DEFAULT_PROBLEM_RECTIFICATION_PAGE_SIZE = 20;
export const DEPARTMENT_RECTIFICATION_FIELD = 'department_rectification_items';
export const DELAY_REQUEST_FIELD = 'problem_rectification_delay_request';
export const DELAY_REQUEST_ITEMS_FIELD = 'problem_rectification_delay_request_items';
export const FLOW_MODE_DEPARTMENT = 'department';
export const FLOW_MODE_BATCH = 'batch';
export const FLOW_MODE_DELAY = 'delay';
......@@ -40,6 +41,8 @@ export const AUDIT_STATUS_PENDING = '未审批';
export const AUDIT_STATUS_APPROVED = '已审批';
export const AUDIT_STATUS_REJECTED = '已驳回';
const FIXED_PROGRESS_VALUES = new Set(['已整改', '已优化']);
const RECTIFICATION_PROGRESS_PRIORITY = ['整改中', '部分整改', '已整改', '已优化', '已完成', '不适用'];
const AUDIT_STATUS_PRIORITY = [AUDIT_STATUS_REJECTED, AUDIT_STATUS_PENDING, AUDIT_STATUS_APPROVED];
const LEVEL_ORDER = ['高', '中', '低'];
const FILTER_FIELD_PATHS = {
progress: 'metadata.rectification_progress',
......@@ -132,18 +135,36 @@ function makeDepartmentItemId(record, department, index) {
return `dept_${recordId}_${index + 1}_${departmentSlug}_${Date.now()}_${randomPart}`;
}
function summarizeTextStatus(values = [], partialText = '部分审批', emptyText = '') {
const normalizedValues = uniqueTruthy(values);
function summarizeLaggingStatus(values = [], priority = [], emptyText = '', unknownRank = 1) {
const normalizedValues = values.map((value) => normalizeText(value));
if (normalizedValues.length === 0) {
return emptyText;
}
if (normalizedValues.length === 1) {
return normalizedValues[0];
}
const priorityRank = new Map(priority.map((status, index) => [status, index]));
const getStatusRank = (status) => {
if (!status) {
return -1;
}
return priorityRank.has(status) ? priorityRank.get(status) : unknownRank;
};
return partialText;
return normalizedValues.reduce((laggingStatus, status) => {
const nextStatus = status || emptyText;
return getStatusRank(nextStatus) < getStatusRank(laggingStatus)
? nextStatus
: laggingStatus;
}, normalizedValues[0] || emptyText);
}
function summarizeDepartmentProgress(values = []) {
return summarizeLaggingStatus(values, RECTIFICATION_PROGRESS_PRIORITY, '', 1);
}
function summarizeDepartmentAuditStatus(values = []) {
return summarizeLaggingStatus(values, AUDIT_STATUS_PRIORITY, AUDIT_STATUS_PENDING, 1);
}
function summarizeDate(values = []) {
......@@ -240,6 +261,9 @@ export function normalizeDelayRequest(value) {
return {
...parsedValue,
item_id: normalizeText(parsedValue.item_id),
source_department_item_id: normalizeText(parsedValue.source_department_item_id),
responsible_department: normalizeText(parsedValue.responsible_department),
original_due_date: parsedValue.original_due_date || null,
requested_due_date: parsedValue.requested_due_date || null,
delay_reason: normalizeText(parsedValue.delay_reason),
......@@ -259,8 +283,41 @@ export function normalizeDelayRequest(value) {
};
}
export function normalizeDelayRequestItems(value) {
let parsedValue = value;
if (typeof value === 'string') {
try {
parsedValue = JSON.parse(value);
} catch (error) {
return [];
}
}
return toArray(parsedValue)
.map((item) => normalizeDelayRequest(item))
.filter(Boolean)
.map((item, index) => ({
...item,
item_id: item.item_id || item.source_department_item_id || `delay_item_${index + 1}`,
responsible_department: item.responsible_department || '未指定责任部门',
}));
}
export function buildDepartmentAssignmentsFromRecord(record) {
const metadata = record?.metadata || record?.raw?.metadata || {};
const departmentItems = normalizeDepartmentRectificationItems(record);
if (departmentItems.length > 0) {
return departmentItems.map((item, index) => ({
item_id: item.item_id || makeDepartmentItemId(record?.raw || record, item.responsible_department, index),
source_department_item_id: item.item_id || '',
responsible_department: normalizeText(item.responsible_department) || '未指定责任部门',
editor: null,
auditor: null,
}));
}
const departments = uniqueTruthy(
toArray(metadata.responsible_department).length > 0
? toArray(metadata.responsible_department)
......@@ -301,6 +358,54 @@ export function buildDepartmentRectificationItems(record, assignments = [], curr
}));
}
export function buildDepartmentDelayRequestItems({
record,
assignments = [],
currentUser = null,
flowRecordId = '',
progressIdEditorByItemId = {},
} = {}) {
const metadata = record?.metadata || record?.raw?.metadata || {};
const departmentItemsById = new Map(
normalizeDepartmentRectificationItems(record).map((item) => [item.item_id, item]),
);
const currentUserId = normalizeUserId(currentUser);
return assignments.map((assignment, index) => {
const sourceDepartmentItemId = normalizeText(assignment.source_department_item_id || assignment.item_id);
const sourceDepartmentItem = departmentItemsById.get(sourceDepartmentItemId);
const itemId =
normalizeText(assignment.item_id) ||
sourceDepartmentItemId ||
makeDepartmentItemId(record?.raw || record, assignment.responsible_department, index);
return {
item_id: itemId,
source_department_item_id: sourceDepartmentItemId,
responsible_department: normalizeText(assignment.responsible_department) || '未指定责任部门',
original_due_date:
sourceDepartmentItem?.the_time_of_rectification ||
metadata.the_time_of_rectification ||
null,
requested_due_date: null,
delay_reason: '',
editor: normalizeUserId(assignment.editor),
auditor: normalizeUserId(assignment.auditor),
sender: currentUserId,
editor_status: EDITOR_STATUS_PENDING,
auditor_status: AUDIT_STATUS_PENDING,
sender_status: AUDIT_STATUS_PENDING,
progress_id_editor: progressIdEditorByItemId[itemId] || '',
progress_id_auditor: '',
progress_id_sender: '',
flow_record_id: flowRecordId,
submitted_at: null,
auditor_decided_at: null,
sender_decided_at: null,
};
});
}
export function buildDepartmentParentMetadataPatch(items = []) {
const normalizedItems = items.filter(Boolean);
const editorIds = uniqueTruthy(normalizedItems.map((item) => item.editor));
......@@ -310,20 +415,14 @@ export function buildDepartmentParentMetadataPatch(items = []) {
[DEPARTMENT_RECTIFICATION_FIELD]: normalizedItems,
fill_in_the_people: editorIds,
review_people: auditorIds,
rectification_progress: summarizeTextStatus(
rectification_progress: summarizeDepartmentProgress(
normalizedItems.map((item) => item.rectification_progress),
'部分整改',
'',
),
audit_status: summarizeTextStatus(
audit_status: summarizeDepartmentAuditStatus(
normalizedItems.map((item) => item.audit_status),
'部分审批',
AUDIT_STATUS_PENDING,
),
final_audit_status: summarizeTextStatus(
final_audit_status: summarizeDepartmentAuditStatus(
normalizedItems.map((item) => item.final_audit_status),
'部分审批',
AUDIT_STATUS_PENDING,
),
the_time_of_rectification: summarizeDate(
normalizedItems.map((item) => item.the_time_of_rectification),
......@@ -668,25 +767,13 @@ export function normalizeProblemRectificationRecord(record) {
const departmentItems = normalizeDepartmentRectificationItems(record);
const hasDepartmentItems = departmentItems.length > 0;
const summarizedProgress = hasDepartmentItems
? summarizeTextStatus(
departmentItems.map((item) => item.rectification_progress),
'部分整改',
'',
)
? summarizeDepartmentProgress(departmentItems.map((item) => item.rectification_progress))
: normalizeText(metadata.rectification_progress);
const summarizedAuditStatus = hasDepartmentItems
? summarizeTextStatus(
departmentItems.map((item) => item.audit_status),
'部分审批',
AUDIT_STATUS_PENDING,
)
? summarizeDepartmentAuditStatus(departmentItems.map((item) => item.audit_status))
: normalizeText(metadata.audit_status);
const summarizedFinalAuditStatus = hasDepartmentItems
? summarizeTextStatus(
departmentItems.map((item) => item.final_audit_status),
'部分审批',
AUDIT_STATUS_PENDING,
)
? summarizeDepartmentAuditStatus(departmentItems.map((item) => item.final_audit_status))
: normalizeText(metadata.final_audit_status);
const summarizedDueDate = hasDepartmentItems
? summarizeDate(departmentItems.map((item) => item.the_time_of_rectification))
......@@ -771,6 +858,7 @@ export function normalizeProblemRectificationRecord(record) {
departmentItems,
hasDepartmentItems,
delayRequest: normalizeDelayRequest(metadata[DELAY_REQUEST_FIELD]),
delayRequestItems: normalizeDelayRequestItems(metadata[DELAY_REQUEST_ITEMS_FIELD]),
isFixed,
isLocked: metadata.is_locked === true,
isOverdue,
......
......@@ -152,6 +152,7 @@ type Record = {
| 主责部门 | `primary_responsible_department` | text | `null` | 否 | 否 | 否 | 否 | 否 |
| 部门整改明细 | `department_rectification_items` | object | `[]` | 否 | 是 | 否 | 否 | 否 |
| 延期申请 | `problem_rectification_delay_request` | subject | `null` | 否 | 否 | 否 | 否 | 否 |
| 部门延期申请明细 | `problem_rectification_delay_request_items` | object | `[]` | 否 | 是 | 否 | 否 | 否 |
| 状态锁定 | `is_locked` | boolean | `false` | 否 | 否 | 否 | 否 | 否 |
### 3.1.1 select 字段选项
......@@ -176,8 +177,9 @@ type Record = {
- 新增驾驶舱扩展字段当前在线上 Subject 中大多仍为 `text` 类型,前端工作台按标准值引导录入,用于支持后续严重程度、问题分类、问题原因和整改质效分析
- 其余 `select` 类型的可选值必须以线上 Subject 配置中的 `settings.options` 为准,禁止在页面内硬编码
- `problem_discovery_situation` 曾在历史文档和归档对象中出现,但本次主对象字段定义未包含;新页面只应在 Subject View 或线上字段配置存在时展示,不能作为 `problem_rectification` 固定字段依赖
- `department_rectification_items` 用于新转发流程的部门级整改明细。单条问题即使只有 1 个责任部门,也生成 1 条明细;转发后 `rectification_progress`、`rectification_situation`、`the_time_of_rectification`、`rectification_proof_materials`、`audit_status`、`final_audit_status` 以明细为事实来源,父字段仅做兼容汇总展示
- `problem_rectification_delay_request` 为问题主记录的单次延期申请嵌套对象,与 `department_rectification_items` 平级;发起延期不要求已有部门整改明细
- `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_items` 为部门级延期申请明细数组;每个责任部门 1 条延期申请,独立保存填写人、一级审批人、最终审批人、申请完成时间、延期事由、状态和待办 ID
- `is_locked` 为问题整改主记录的状态锁定标记;仅整改进度汇总值为 `已整改` 的记录允许写入 `true`,锁定后所有业务字段和流程状态均不可修改,只有解锁操作可以将其写回 `false`
- 部门明细的填写人只能编辑自己负责明细的整改业务字段,送审后(`editor_status = "已完成"`)不再允许编辑;审批人审批通过后(`audit_status = "已审批"`)不再允许编辑,审批驳回后(`audit_status = "已驳回"`)审批人可以编辑该明细的整改业务字段,填写人也恢复编辑。审批人恢复的只是业务字段编辑权限,不获得填写人的送审权限。流程推送人可以编辑本次推送关联的父问题记录及全部部门明细整改业务字段。审批状态、待办 ID、明细 ID 等流程控制字段不作为普通表单字段开放
- 推送人修改父问题记录或部门明细时,不自动重置该明细已经形成的审批状态,也不自动重新生成流程待办
......@@ -204,14 +206,18 @@ type Record = {
| 更新人 | `updated_by` | user | 否 | 最近更新用户 `_id` |
| 更新时间 | `updated_at` | date | 否 | 最近更新时间 |
### 3.1.4 `problem_rectification_delay_request` 嵌套对象
### 3.1.4 `problem_rectification_delay_request` / `problem_rectification_delay_request_items` 嵌套对象
- **subject**: `problem_rectification_delay_request`
- **父级路径**: `metadata.problem_rectification_delay_request`
- **部门级父级路径**: `metadata.problem_rectification_delay_request_items[]`
| 字段标签 | 字段别名 | 字段类型 | 是否必填 | 说明 |
| --- | --- | --- | --- | --- |
| 原预计完成时间 | `original_due_date` | date | 否 | 发起申请时从问题主记录 `the_time_of_rectification` 复制,保存 ISO 8601 |
| 明细 ID | `item_id` | text | 部门级必填 | 部门延期申请唯一标识,用于行级保存、审批和待办回写 |
| 关联部门整改明细 ID | `source_department_item_id` | text | 否 | 有 `department_rectification_items` 时保存对应 `item_id`,最终审批通过后用于回写该部门预计完成时间 |
| 责任部门 | `responsible_department` | text | 部门级必填 | 当前延期申请对应的责任部门 |
| 原预计完成时间 | `original_due_date` | date | 否 | 发起申请时优先从对应部门明细 `the_time_of_rectification` 复制,无部门明细时从问题主记录复制,保存 ISO 8601 |
| 申请完成时间 | `requested_due_date` | date | 否 | 填写人填写;提交申请时必填并保存 ISO 8601 |
| 延期事由 | `delay_reason` | long_text | 否 | 填写人填写;提交申请时必填 |
| 填写人 | `editor` | user | 否 | 保存推送时选择的填写人 `_id` |
......@@ -230,11 +236,11 @@ type Record = {
写入约定:
- 延期申请发起时只初始化问题主记录的嵌套对象和填写人 OA 待办,不修改主记录 `the_time_of_rectification`
- 延期申请发起时初始化问题主记录的部门级延期申请数组和填写人 OA 待办,不修改主记录或部门明细的 `the_time_of_rectification`
- `requested_due_date`、`delay_reason` 在保存草稿时可以为空,在提交申请时由前端强制校验必填
- 一级审批或最终审批驳回后,`editor_status` 重置为 `未完成`,并重新创建填写人 OA 待办
- 只有 `sender_status = "已审批"` 时,才把 `requested_due_date` 写回问题主记录 `the_time_of_rectification`
- 同一问题主记录已有该嵌套对象时,不允许再次创建另一份延期申请
- 一级审批或最终审批驳回后,仅对应部门延期申请的 `editor_status` 重置为 `未完成`,并重新创建该部门填写人 OA 待办
- 只有部门延期申请 `sender_status = "已审批"` 时,才把该行 `requested_due_date` 写回对应 `department_rectification_items[].the_time_of_rectification`;如果不存在对应部门整改明细,则回退写回问题主记录 `the_time_of_rectification`
- 同一问题主记录已有 `problem_rectification_delay_request` 或 `problem_rectification_delay_request_items` 时,不允许再次创建另一份延期申请
### 3.1.5 metadata 示例
......@@ -363,8 +369,8 @@ type Record = {
- `restriction_value` 保存被推送整改记录的 `_id[]`
- `reference_record_id` 与 `restriction_value` 保持一致,作为显式的“关联记录 ID”冗余字段
- `subject_name` 保存被推送记录所属对象名,当前整改流程固定为 `problem_rectification`
- `flow_mode` 区分流程:`department` 表示单条问题按责任部门生成整改明细行,`batch` 表示多选问题的单填写人 / 单审批人简单推送,`delay` 表示问题级延期申请;历史 `delay_department` 值仅保留页面识别兼容
- `department_item_ids` 保存本次部门流程涉及的 `department_rectification_items[].item_id`
- `flow_mode` 区分流程:`department` 表示单条问题按责任部门生成整改明细行,`batch` 表示多选问题的单填写人 / 单审批人简单推送,`delay` 表示历史问题级延期申请,`delay_department` 表示按责任部门生成延期申请明细
- `department_item_ids` 保存本次部门流程涉及的部门级 `item_id`;整改流程对应 `department_rectification_items[].item_id`,延期流程对应 `problem_rectification_delay_request_items[].item_id`
- `progress_id_editor`、`progress_id_auditor`、`progress_id_sender` 用于记录每一步用户确认动作生成的流程 ID,用来区分当前流程推进到了哪一步
- `editor_status`:当前项目统一使用 `未完成`、`已完成`
- `auditor_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