审批流程:用户名改为常见人名 + 按销售归属的审批线指派(软约束)

- 用户名改为常见人名(张伟/王芳/李娜/刘洋/陈静/赵磊/孙莉/周强)
- 审批线模型:每个销售→指定风控+管理层审批人(含默认线兜底),resolveAssignees 纯函数+4单测
- 提交时按销售归属计算并落库指派(assessment_assignments 表)
- 软约束:待办默认只看分给我的(含未指派),同角色他人仍可代审;详情页显示指派审批人
- 审批流程页新增「审批人指派·审批线」配置区(启用/默认线/按销售配线)
- 配置 GET/PUT 扩展 assignment;getApprovalConfig 向后兼容回填
This commit is contained in:
freedakgmail
2026-06-13 18:26:48 +08:00
parent 757b9c4a69
commit 11997e6104
11 changed files with 364 additions and 16 deletions
+52 -9
View File
@@ -17,8 +17,8 @@ import {
space,
typographyStyle,
} from '../design-system/components/styles.js';
import { fetchAssessmentsPage, fetchSummary, archiveAssessment, applyCalibration, listDrafts, deleteDraftApi, API_BASE } from '../api/client.js';
import type { AssessmentListItem, WorkflowStatus, DraftItem } from '../api/client.js';
import { fetchAssessmentsPage, fetchSummary, archiveAssessment, applyCalibration, listDrafts, deleteDraftApi, fetchAssignments, API_BASE } from '../api/client.js';
import type { AssessmentListItem, WorkflowStatus, DraftItem, AssignmentRecord } from '../api/client.js';
import { useAuthStore } from '../stores/authStore.js';
import { GuideBanner } from '../app/Guidance.js';
import { RiskBadge } from '../charts/index.js';
@@ -104,6 +104,9 @@ export function Dashboard(): JSX.Element {
// 待办列表
const [todoItems, setTodoItems] = useState<AssessmentListItem[]>([]);
// 审批人指派(assessmentId → 记录);待办软过滤用。
const [assignments, setAssignments] = useState<Record<string, AssignmentRecord>>({});
const [onlyMine, setOnlyMine] = useState(true);
// 草稿箱(销售:未运行/未提交的向导进度,服务端持久化)
const [drafts, setDrafts] = useState<DraftItem[]>([]);
// 统计
@@ -160,6 +163,12 @@ export function Dashboard(): JSX.Element {
} else {
setDrafts([]);
}
// 审批人指派(风控/管理层待办软过滤用)。
if (role === '风控' || role === '管理层') {
fetchAssignments().then(setAssignments).catch(() => setAssignments({}));
} else {
setAssignments({});
}
}, [role, user?.username]);
/** 删除一条草稿并刷新草稿箱。 */
@@ -507,13 +516,47 @@ export function Dashboard(): JSX.Element {
</div>
)}
{todoItems.length > 0 && (
<div style={{ marginBottom: `${space(4)}px` }}>
<Card title={`待处理 (${todoCount})`}>
<Table columns={columns} data={todoItems} getRowKey={(row) => row.id} caption={`${role} 待处理列表`} emptyMessage="当前没有需要你处理的评估" />
</Card>
</div>
)}
{todoItems.length > 0 && (() => {
const myName = user?.username;
const assignedToMe = (id: string): boolean => {
const a = assignments[id];
if (a === undefined) return false;
return role === '风控' ? a.riskReviewerName === myName : role === '管理层' ? a.managerName === myName : false;
};
const isAssigned = (id: string): boolean => {
const a = assignments[id];
if (a === undefined) return false;
return role === '风控' ? a.riskReviewerName !== null : role === '管理层' ? a.managerName !== null : false;
};
// 软约束:默认只看分给我的;未指派的也展示(避免遗漏)。
const shown = onlyMine ? todoItems.filter((t) => assignedToMe(t.id) || !isAssigned(t.id)) : todoItems;
const assignCol: TableColumn<AssessmentListItem> = {
key: 'assignee',
header: '指派审批人',
render: (r) => {
const a = assignments[r.id];
const name = a !== undefined ? (role === '管理层' ? a.managerName : a.riskReviewerName) : null;
if (name === null || name === undefined) return <span style={{ color: colorVar('color.text.secondary') }}></span>;
const mine = name === myName;
return <span style={{ ...typographyStyle('caption'), fontWeight: 600, color: mine ? '#15803D' : colorVar('color.text.primary') }}>{name}{mine ? '(我)' : ''}</span>;
},
};
const todoColumns = [...columns.slice(0, -1), assignCol, columns[columns.length - 1]!];
return (
<div style={{ marginBottom: `${space(4)}px` }}>
<Card title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: `${space(2)}px` }}>
<span> ({shown.length})</span>
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, ...typographyStyle('caption'), color: colorVar('color.text.secondary'), fontWeight: 400 }}>
<input type="checkbox" checked={onlyMine} onChange={(e) => setOnlyMine(e.target.checked)} />
</label>
</div>
}>
<Table columns={todoColumns} data={shown} getRowKey={(row) => row.id} caption={`${role} 待处理列表`} emptyMessage={onlyMine ? '没有分给你的待处理项(可取消勾选查看全部)' : '当前没有需要你处理的评估'} />
</Card>
</div>
);
})()}
{role === '商务/销售' && drafts.length > 0 && (
<div style={{ marginBottom: `${space(4)}px` }}>