Commit 88f229a6 authored by David Yang's avatar David Yang

fix: bugs

parent dcdc9689
No preview for this file type
......@@ -16,6 +16,8 @@ const ROUTE_PATHS = {
export default defineConfig({
layout: false,
fastRefresh: true,
base: '/problem_rectification_new/',
publicPath: '/problem_rectification_new/',
alias: {
react: resolvePackageRoot('react'),
'react-dom': resolvePackageRoot('react-dom'),
......
{
"name": "problem_rectification_new",
"version": "1.0.3",
"build": "abc",
"description": "merge master",
"date": "2025-07-14 19:27:20",
"author": "yang"
}
This diff is collapsed.
No preview for this file type
This diff is collapsed.
......@@ -15,25 +15,6 @@ import {
isViewAccessible,
} from '@/utils/subjectView';
function formatDateTime(value) {
if (!value) {
return '未记录';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '未记录';
}
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
function CloseIcon() {
return (
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="size-5">
......@@ -141,20 +122,6 @@ const RecordDetailDrawer = (props, ref) => {
const activeView = detailViews.find((view) => view.name === activeTabKey) || detailViews[0];
const drawerWidth = isFullscreen ? '100vw' : typeof width === 'number' ? `${width}px` : width;
const summaryItems = [
{
label: '主题',
value: subject?.title || subject?.name || '未识别',
},
{
label: '当前视图',
value: activeView?.title || activeView?.name || '基础信息',
},
{
label: '最近更新',
value: formatDateTime(editableRecord?.updated_at),
},
];
const closeDrawer = () => {
onClose?.();
......@@ -176,40 +143,32 @@ const RecordDetailDrawer = (props, ref) => {
className="pointer-events-auto h-full transform overflow-hidden border-l border-white/70 bg-[linear-gradient(180deg,#fffdf8_0%,#f8fafc_38%,#f4f4f5_100%)] shadow-[0_24px_80px_rgba(15,23,42,0.24)] ring-1 ring-zinc-950/5 transition duration-300 data-closed:translate-x-full data-enter:ease-out data-leave:ease-in"
>
<div className="flex h-full flex-col">
<header className="relative overflow-hidden border-b border-white/70 bg-[linear-gradient(135deg,rgba(255,255,255,0.95),rgba(255,247,237,0.9)_45%,rgba(244,244,245,0.95))] px-6 pb-5 pt-6 shadow-[0_8px_24px_rgba(15,23,42,0.06)] backdrop-blur">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-rose-200 to-transparent" />
<div className="flex items-start justify-between gap-4">
<div className="min-w-0 flex-1">
<div className="text-[11px] font-semibold uppercase tracking-[0.32em] text-rose-500">
Record Detail
</div>
<Headless.DialogTitle className="mt-2 truncate text-[28px] font-semibold tracking-tight text-zinc-950">
<Headless.DialogTitle className="sr-only">
{editableRecord?.title || editableRecord?.name || '记录详情'}
</Headless.DialogTitle>
<div className="mt-2 flex flex-wrap items-center gap-2 text-sm text-zinc-500">
<span className="inline-flex rounded-full bg-white/90 px-3 py-1 font-medium text-zinc-700 ring-1 ring-white shadow-sm">
{readonly ? '只读模式' : '可编辑'}
</span>
{editableRecord?.status ? (
<span className="inline-flex rounded-full bg-rose-50 px-3 py-1 font-medium text-rose-700 ring-1 ring-rose-100">
{editableRecord.status}
</span>
) : null}
</div>
<div className="mt-4 grid gap-3 md:grid-cols-3">
{summaryItems.map((item) => (
<div
key={item.label}
className="rounded-2xl border border-white/80 bg-white/72 px-4 py-3 shadow-sm ring-1 ring-zinc-950/5"
<header className="relative border-b border-white/70 bg-white/84 px-6 py-4 shadow-[0_8px_24px_rgba(15,23,42,0.05)] backdrop-blur">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-rose-200 to-transparent" />
<div className="flex items-center justify-between gap-4">
<div className="min-w-0 flex-1">
<div className="flex gap-2 overflow-x-auto">
{detailViews.map((view) => {
const selected = view.name === activeView?.name;
return (
<button
key={view.name}
type="button"
onClick={() => setActiveTabKey(view.name)}
className={clsx(
'shrink-0 rounded-full px-4 py-2 text-sm font-medium transition',
selected
? 'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg shadow-rose-200/60'
: 'bg-white/92 text-zinc-600 ring-1 ring-zinc-200/80 hover:bg-rose-50 hover:text-rose-700',
)}
>
<div className="text-xs font-medium uppercase tracking-[0.16em] text-zinc-400">
{item.label}
</div>
<div className="mt-1 truncate text-sm font-semibold text-zinc-800">
{item.value}
</div>
</div>
))}
{view.title || view.name}
</button>
);
})}
</div>
</div>
<div className="flex shrink-0 gap-2 rounded-full border border-white/80 bg-white/88 p-1.5 shadow-sm ring-1 ring-zinc-950/5 backdrop-blur">
......@@ -231,29 +190,6 @@ const RecordDetailDrawer = (props, ref) => {
</button>
</div>
</div>
<div className="mt-5">
<div className="flex gap-2 overflow-x-auto">
{detailViews.map((view) => {
const selected = view.name === activeView?.name;
return (
<button
key={view.name}
type="button"
onClick={() => setActiveTabKey(view.name)}
className={clsx(
'shrink-0 rounded-full px-4 py-2 text-sm font-medium transition',
selected
? 'bg-[linear-gradient(135deg,#7f1d1d,#be123c)] text-white shadow-lg shadow-rose-200/60'
: 'bg-white/92 text-zinc-600 ring-1 ring-zinc-200/80 hover:bg-rose-50 hover:text-rose-700',
)}
>
{view.title || view.name}
</button>
);
})}
</div>
</div>
</header>
<main className="min-h-0 flex-1 overflow-y-auto bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.9),rgba(248,250,252,0.96)_32%,rgba(244,244,245,0.98)_100%)] px-6 py-5">
......
......@@ -14,6 +14,7 @@ import RecordDetailDrawer from '@/pages/RecordDetailDrawer';
import {
batchRemoveRecords,
batchUpdateRecords,
cloneRecord,
createNotice,
createRecord,
executeAction,
......@@ -491,13 +492,16 @@ export default function ProblemRectificationListPage() {
const [selectedIds, setSelectedIds] = useState([]);
const [showCreateModal, setShowCreateModal] = useState(false);
const [showPushModal, setShowPushModal] = useState(false);
const [cloneTargetRecord, setCloneTargetRecord] = useState(null);
const [formValues, setFormValues] = useState({});
const [fillerUsers, setFillerUsers] = useState([]);
const [auditorUsers, setAuditorUsers] = useState([]);
const [successMessage, setSuccessMessage] = useState('');
const reloadList = useCallback(async () => {
setLoading(true);
setError('');
setSuccessMessage('');
try {
const resources = await loadProblemRectificationListResources();
......@@ -590,6 +594,7 @@ export default function ProblemRectificationListPage() {
const pendingAuditCount = filteredRecords.filter(
(item) => item.auditStatus && item.auditStatus !== '已审批',
).length;
const quickSelectEnabled = activeGridView?.settings?.quickSelect !== false;
const handleCreate = async () => {
if (
......@@ -657,6 +662,34 @@ export default function ProblemRectificationListPage() {
}
};
const handleClone = async () => {
if (!cloneTargetRecord) {
return;
}
const recordName = cloneTargetRecord.name || cloneTargetRecord.id;
if (!recordName) {
setError('当前记录缺少可复制的 name 标识。');
return;
}
setActionLoading(true);
setError('');
setSuccessMessage('');
try {
await cloneRecord(recordName);
const title = cloneTargetRecord.title || cloneTargetRecord.projectName || cloneTargetRecord.name || '当前记录';
setCloneTargetRecord(null);
await reloadList();
setSuccessMessage(`已复制“${title}”,新记录已加入当前列表。`);
} catch (requestError) {
setError(requestError?.message || '复制问题整改记录失败,请稍后重试。');
} finally {
setActionLoading(false);
}
};
const handleSearchUsers = useCallback(async (keyword) => {
const filter = keyword.trim()
? {
......@@ -916,9 +949,11 @@ export default function ProblemRectificationListPage() {
title="问题清单"
description={`当前视图显示 ${filteredRecords.length} 条记录,已选中 ${selectedIds.length} 条,其中重点后果问题 ${consequenceCount} 条、逾期未完成 ${overdueCount} 条。`}
columns={configuredView.columns || []}
columnGroups={activeGridView?.settings?.groups || []}
records={filteredRecords}
selectedIds={selectedIds}
onSelectionChange={setSelectedIds}
selectable={quickSelectEnabled}
toolbarActions={[
{
key: 'create',
......@@ -964,6 +999,13 @@ export default function ProblemRectificationListPage() {
variant: 'plain',
onClick: ({ openDetail, record }) => openDetail(record),
},
{
key: 'clone',
label: '复制',
variant: 'outline',
disabled: () => actionLoading,
onClick: ({ record }) => setCloneTargetRecord(record),
},
{
key: 'delete',
label: '删除',
......@@ -988,6 +1030,7 @@ export default function ProblemRectificationListPage() {
states={{
loading,
error,
success: successMessage,
loadingTitle: '正在加载问题整改列表',
loadingDescription: '正在读取对象结构、视图配置和记录数据,请稍候。',
emptyTitle: '当前视图暂无数据',
......@@ -1116,6 +1159,31 @@ export default function ProblemRectificationListPage() {
</DialogActions>
</Dialog>
) : null}
{cloneTargetRecord ? (
<Dialog open={Boolean(cloneTargetRecord)} onClose={() => setCloneTargetRecord(null)} size="lg">
<DialogTitle>复制问题整改记录</DialogTitle>
<DialogDescription>
将基于当前记录复制一条新记录,复制完成后会刷新列表。
</DialogDescription>
<DialogBody>
<div className="rounded-[24px] border border-zinc-200 bg-zinc-50/80 p-4">
<div className="text-sm font-medium text-slate-900">待复制记录</div>
<div className="mt-2 text-sm leading-6 text-slate-600">
{cloneTargetRecord.title || cloneTargetRecord.projectName || cloneTargetRecord.name || '当前记录'}
</div>
</div>
</DialogBody>
<DialogActions>
<Button outline disabled={actionLoading} onClick={() => setCloneTargetRecord(null)}>
取消
</Button>
<Button color="blue" disabled={actionLoading} onClick={handleClone}>
{actionLoading ? '复制中...' : '确认复制'}
</Button>
</DialogActions>
</Dialog>
) : null}
</>
);
}
......@@ -56,7 +56,9 @@
问题整改主列表与详情抽屉统一以 `getSubjectView(subjectName)` 返回的视图配置驱动,不再在页面内写死部门标签和字段顺序:
- `type = "grid"` 的视图用于主列表顶部视图切换,替代原先写死的 tab
- `settings.columns` 决定列表列顺序、宽度和排序优先级
- `settings.columns` 决定列表列顺序、宽度、固定列位置和排序优先级,列表页不得在视图列后追加自定义业务字段列
- `settings.groups` 决定列表列分组,例如把多个问题分类字段归入同一分组;前端需展示分组表头,同一分组下的列即使在 `settings.columns` 中不连续,也应在表格内聚拢展示,并支持每个分组独立折叠 / 展开,折叠后隐藏该分组子列
- `settings.quickSelect` 决定列表是否展示勾选列,用于批量推送、删除等操作
- `settings.restrictions` 与 `settings.filters.conditions` 共同决定该视图下可见的数据范围
- `settings.viewAuth` 决定当前用户是否能看到该视图
- `type = "detail"` 的视图用于详情抽屉字段编排,按 `columns` 顺序渲染字段
......@@ -69,6 +71,8 @@
- `problem_rectification` 与 `issue_rectification_archive` 列表页统一复用 `RecordWorkbench` 作为“结构化表格 + 工具栏 + 详情抽屉入口”的工作台容器
- 页面本身只负责提供 subject 对应的数据加载、统计口径、筛选 schema、行操作与批量操作
- 详情侧栏统一复用 `RecordDetailDrawer`,通过 `recordName`、`subjectName`、`preferredViewName` 驱动,不再在各页面重复实现抽屉骨架
- 详情侧栏顶部不展示记录标题、状态和摘要卡片,只保留详情视图切换与全屏 / 关闭操作,字段主体作为主要阅读区域
- 列表行操作列使用图标按钮横向排列,查看、复制、删除等常用操作通过 tooltip / aria-label 标明含义,避免文字按钮纵向堆叠
- 新增同类列表页时,优先复用工作台与详情抽屉组合,避免再次复制问题整改页 JSX
### 3.1 目标页面拆分
......@@ -134,10 +138,11 @@
旧应用 `problem_rectification` 列表配置中实际暴露了 5 个操作:
1. 查看详情
2. 删除数据
3. 推送问题整改表
4. 导出问题整改表
5. 一键归档
2. 复制数据
3. 删除数据
4. 推送问题整改表
5. 导出问题整改表
6. 一键归档
这些操作分别对应如下业务含义:
......@@ -146,12 +151,19 @@
- 查看问题记录完整字段
- 支持后续在详情中编辑整改情况、证明材料、审批状态
### 5.2 删除数据
### 5.2 复制数据
- 在问题整改列表的单行操作中触发
- 弹窗确认后调用 `cloneRecord(record.name)`,由 `/record/clone` 基于当前记录生成新记录
- 复制完成后刷新当前列表,并展示成功提示
- 复制操作不改变原记录的整改、复核或归档状态
### 5.3 删除数据
- 支持删除单条或批量删除选中项
- 本质调用 `/record/batch/remove`
### 5.3 推送问题整改表
### 5.4 推送问题整改表
- 从已选问题记录中选择填报人和审核人
- 创建一条 `message_reference_id` 流程主单
......@@ -160,19 +172,19 @@
- 给填报人发送内部通知
- 创建 OA 待办,链接指向 `/problem_rectification_form?id=<message_record_name>`
### 5.4 导出问题整改表
### 5.5 导出问题整改表
- 按选中问题导出 Excel
- 导出字段来自 `problem_rectification.metadata`
- 本质是把主表字段映射成报表列,不参与状态流转
### 5.5 一键归档
### 5.6 一键归档
- 将选中问题记录导入 `problem_rectification_archive_records`
- 再创建一条 `issue_rectification_archive` 记录,保存归档明细引用列表
- 归档的本质是生成快照,不应直接依赖原始问题记录后续继续存在
### 5.6 旧多维表格能力盘点
### 5.7 旧多维表格能力盘点
实际页面检查后,旧系统“问题整改”和“往期材料归档”页都运行在同一套多维表格容器上,页面共有能力包括:
......@@ -187,6 +199,7 @@
- 搜索 / 筛选
- 批量勾选
- 查看详情
- 复制数据
- 推送整改表
- 导出
- 一键归档
......@@ -444,7 +457,7 @@
当前图表与字段映射:
- 问题类型 / 业务领域分布:优先使用 `problem_category_level_1` 至 `problem_category_level_4` 生成矩形树图;若分类未填写,则回退到 `related_issue_domain`
- 问题类型 / 业务领域分布:使用 `problem_category_level_1` 至 `problem_category_level_4` 分别生成四个层级 tab,切换后以横向柱状图展示当前层级各分类数量;一级分类缺失时回退到 `related_issue_domain`,其余层级缺失时归入“未分类”
- 问题根本原因分析:使用 `problem_root_cause` 聚合
- 问题来源对比:以 `problem_from` 为横轴,并按 `inspect_the_implementing_unit` 做堆叠比较
- 重点问题集中展示:
......@@ -479,17 +492,16 @@
当前图表与字段映射:
- 问题整改时效:按责任部门计算 `rectification_timeliness_level` 平均分,低=1、中=2、高=3
- 问题整改质量:按责任部门计算 `rectification_material_quality_level` 平均分,低=1、中=2、高=3
- 问题整改助力总结:
- 制度修订数量:`involves_policy_revision = 是`
- 系统建设数量:`involves_system_construction = 是`
- 风险-质量热力图:
- X 轴:`problem_severity_level`
- Y 轴:`rectification_overall_rating`
- 部门综合整改评定等级排行:
- 以责任部门为维度统计 `rectification_overall_rating` 的低/中/高占比
- 按高评级占比降序展示
- 驾驶舱只展示最终“综合整改质效结果”的高 / 中 / 低分布,不再展示整改时间评估、整改证明材料质量、制度修订、系统建设、往期发现、重复出现等明细维度
- 明细维度仍在维护页录入和校验,包括:
- `rectification_timeliness_level`:整改时间评估,低 / 中 / 高
- `rectification_material_quality_level`:整改证明材料质量,低 / 中 / 高
- `involves_policy_revision`、`policy_revision_name`:是否涉及制度修订及制度名称
- `involves_system_construction`、`system_construction_name`:是否涉及系统建设及系统名称
- `is_historical_problem`:是否为往期发现问题
- `is_repeated_problem`:问题是否重复出现
- `rectification_overall_rating`:综合整改评定等级,低 / 中 / 高
- 当前驾驶舱优先使用已维护的 `rectification_overall_rating`;若该字段为空,则按初步规则从明细维度推导结果:两个及以上高为高;出现低则低;一个高则中。具体参与维度和最终规则后续需业务确认
---
......
......@@ -35,11 +35,6 @@ const FIXED_PROGRESS_VALUES = new Set(['已整改', '已优化']);
const POSITIVE_FLAGS = new Set(['是', '有', '涉及', 'TRUE', '1', 'Y', 'YES']);
const SEVERITY_LEVELS = ['低', '中', '高'];
const RATING_LEVELS = ['低', '中', '高'];
const SCORE_LEVEL_MAP = {
低: 1,
中: 2,
高: 3,
};
function normalizeText(value) {
return String(value ?? '').trim();
......@@ -113,6 +108,42 @@ function isFixedProblem(record) {
return FIXED_PROGRESS_VALUES.has(normalizeText(record?.metadata?.rectification_progress));
}
function normalizeRatingLevel(value) {
const text = normalizeText(value);
return RATING_LEVELS.includes(text) ? text : '';
}
function calculateQualityRating(record) {
const explicitRating = normalizeRatingLevel(record.overallRating);
if (explicitRating) {
return explicitRating;
}
const dimensions = [
normalizeRatingLevel(record.timelinessLevel),
normalizeRatingLevel(record.materialQualityLevel),
record.involvesPolicyRevision ? '高' : '',
record.involvesSystemConstruction ? '高' : '',
record.isHistoricalProblem ? '低' : '',
record.isRepeatedProblem ? '低' : '',
].filter(Boolean);
if (dimensions.length === 0) {
return '';
}
if (dimensions.includes('低')) {
return '低';
}
const highCount = dimensions.filter((item) => item === '高').length;
if (highCount >= 2) {
return '高';
}
return '中';
}
function extractPrimaryDepartment(metadata) {
const primary = normalizeText(metadata.primary_responsible_department);
if (primary) {
......@@ -151,39 +182,16 @@ function buildDistributionMap(records, getKey) {
);
}
function upsertTreeNode(nodes, name) {
let existing = nodes.find((node) => node.name === name);
if (!existing) {
existing = { name, value: 0, children: [] };
nodes.push(existing);
}
existing.value += 1;
return existing;
}
function buildCategoryTreemap(records) {
const root = [];
records.forEach((record) => {
const path = [
record.categoryLevel1,
record.categoryLevel2,
record.categoryLevel3,
record.categoryLevel4,
].filter(Boolean);
if (path.length === 0) {
path.push(record.relatedIssueDomain || '未分类');
}
let currentNodes = root;
path.forEach((name) => {
const node = upsertTreeNode(currentNodes, name);
currentNodes = node.children;
});
});
return root;
function buildCategoryDistributions(records) {
return {
level1: buildDistributionMap(
records,
(item) => item.categoryLevel1 || item.relatedIssueDomain || '未分类',
),
level2: buildDistributionMap(records, (item) => item.categoryLevel2 || '未分类'),
level3: buildDistributionMap(records, (item) => item.categoryLevel3 || '未分类'),
level4: buildDistributionMap(records, (item) => item.categoryLevel4 || '未分类'),
};
}
function buildSourceComparison(records) {
......@@ -306,15 +314,6 @@ function collectDepartmentStats(records) {
totalCount: 0,
fixedCount: 0,
overdueCount: 0,
timelinessScoreTotal: 0,
timelinessScoreCount: 0,
qualityScoreTotal: 0,
qualityScoreCount: 0,
ratingCounts: {
低: 0,
中: 0,
高: 0,
},
});
}
......@@ -328,36 +327,12 @@ function collectDepartmentStats(records) {
if (record.isOverdue) {
stats.overdueCount += 1;
}
const timelinessScore = SCORE_LEVEL_MAP[record.timelinessLevel];
if (timelinessScore) {
stats.timelinessScoreTotal += timelinessScore;
stats.timelinessScoreCount += 1;
}
const qualityScore = SCORE_LEVEL_MAP[record.materialQualityLevel];
if (qualityScore) {
stats.qualityScoreTotal += qualityScore;
stats.qualityScoreCount += 1;
}
if (record.overallRating && stats.ratingCounts[record.overallRating] !== undefined) {
stats.ratingCounts[record.overallRating] += 1;
}
});
});
return Array.from(departmentMap.values()).map((item) => ({
...item,
completionRate: item.totalCount > 0 ? (item.fixedCount / item.totalCount) * 100 : 0,
timelinessAverage:
item.timelinessScoreCount > 0
? item.timelinessScoreTotal / item.timelinessScoreCount
: 0,
qualityAverage:
item.qualityScoreCount > 0
? item.qualityScoreTotal / item.qualityScoreCount
: 0,
}));
}
......@@ -412,58 +387,25 @@ function buildTimelinessScatter(records) {
.slice(0, 120);
}
function buildScoreRanking(departmentStats, key) {
return [...departmentStats]
.filter((item) => item[key] > 0)
.sort((left, right) => right[key] - left[key])
.map((item) => ({
name: item.name,
value: Number(item[key].toFixed(2)),
}));
}
function buildSeverityRatingHeatmap(records) {
const matrix = [];
RATING_LEVELS.forEach((rating, yIndex) => {
SEVERITY_LEVELS.forEach((severity, xIndex) => {
const count = records.filter(
(record) =>
record.severityLevel === severity && record.overallRating === rating,
).length;
matrix.push([xIndex, yIndex, count]);
});
});
return {
xLabels: SEVERITY_LEVELS,
yLabels: RATING_LEVELS,
data: matrix,
function buildQualityRatingDistribution(records) {
const counts = {
高: 0,
中: 0,
低: 0,
};
}
function buildDepartmentRatingDistribution(departmentStats) {
return [...departmentStats]
.filter((item) => item.totalCount > 0)
.map((item) => {
const high = item.ratingCounts.高 || 0;
const medium = item.ratingCounts.中 || 0;
const low = item.ratingCounts.低 || 0;
const ratedTotal = high + medium + low;
records.forEach((record) => {
if (record.qualityRating && counts[record.qualityRating] !== undefined) {
counts[record.qualityRating] += 1;
}
});
return {
name: item.name,
low,
medium,
high,
lowPercent: ratedTotal > 0 ? (low / ratedTotal) * 100 : 0,
mediumPercent: ratedTotal > 0 ? (medium / ratedTotal) * 100 : 0,
highPercent: ratedTotal > 0 ? (high / ratedTotal) * 100 : 0,
highShare: ratedTotal > 0 ? high / ratedTotal : 0,
};
})
.sort((left, right) => right.highShare - left.highShare);
return ['高', '中', '低']
.map((level) => ({
name: level,
value: counts[level],
}))
.filter((item) => item.value > 0);
}
function summarizeRecords(records) {
......@@ -481,19 +423,14 @@ function summarizeRecords(records) {
overdueCount,
overdueRate,
progressDistribution: buildDistributionMap(records, (item) => item.progress || '未填写'),
categoryTreemap: buildCategoryTreemap(records),
categoryDistributions: buildCategoryDistributions(records),
rootCauseDistribution: buildDistributionMap(records, (item) => item.rootCause || '未分类原因'),
sourceComparison: buildSourceComparison(records),
focusIssueGroups: buildFocusIssueGroups(records),
overdueRanking: buildOverdueRanking(departmentStats),
departmentMatrix: buildDepartmentMatrix(departmentStats),
timelinessScatter: buildTimelinessScatter(records),
timelinessScoreRanking: buildScoreRanking(departmentStats, 'timelinessAverage'),
qualityScoreRanking: buildScoreRanking(departmentStats, 'qualityAverage'),
policyRevisionCount: records.filter((item) => item.involvesPolicyRevision).length,
systemConstructionCount: records.filter((item) => item.involvesSystemConstruction).length,
severityRatingHeatmap: buildSeverityRatingHeatmap(records),
departmentRatingDistribution: buildDepartmentRatingDistribution(departmentStats),
qualityRatingDistribution: buildQualityRatingDistribution(records),
};
}
......@@ -514,7 +451,7 @@ function normalizeProblemRecord(record) {
const delayedByPending = dueDate && dueDate.isBefore(now, 'day') && !isFixed;
const isDelayed = Boolean(delayedByProgress || delayedByActual || delayedByPending);
return {
const normalizedRecord = {
id: record?._id || record?.name || '',
title: normalizeText(record?.title) || normalizeText(metadata.project_name) || '-',
projectName: normalizeText(metadata.project_name),
......@@ -562,6 +499,11 @@ function normalizeProblemRecord(record) {
isDelayed,
isOverdue: delayedByPending,
};
return {
...normalizedRecord,
qualityRating: calculateQualityRating(normalizedRecord),
};
}
function buildDepartmentBuckets(records) {
......
......@@ -22,13 +22,6 @@ import {
export const PROBLEM_RECTIFICATION_SUBJECT = 'problem_rectification';
const FIXED_PROGRESS_VALUES = new Set(['已整改', '已优化']);
const LEVEL_ORDER = ['高', '中', '低'];
const WORKBENCH_ENHANCED_COLUMN_FIELDS = [
'problem_severity_level',
'rectification_overall_rating',
'primary_responsible_department',
'problem_root_cause',
'actual_rectification_completed_at',
];
const WORKBENCH_PRIORITY_CREATE_FIELD_NAMES = [
'problem_severity_level',
'problem_category_level_1',
......@@ -263,28 +256,9 @@ export function buildGridViews(views = []) {
}
export function buildViewColumnsForList(view, subject) {
const baseColumns = buildViewColumns(view, subject).filter(
return buildViewColumns(view, subject).filter(
(column) => column.field !== 'title' || !column.hide,
);
const existingFields = new Set(baseColumns.map((column) => getFieldNameFromPath(column.field)));
const extraColumns = WORKBENCH_ENHANCED_COLUMN_FIELDS
.filter((fieldName) => !existingFields.has(fieldName))
.map((fieldName) => {
const fieldDef = findSubjectField(subject, fieldName);
if (!fieldDef) {
return null;
}
return {
field: `metadata.${fieldDef.name}`,
label: fieldDef.label || fieldDef.name,
width: 168,
fieldDef,
};
})
.filter(Boolean);
return [...baseColumns, ...extraColumns];
}
export function getRecordCellText(record, column) {
......
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