汇总/分析卡按用户口径对齐:销售统计仅本人;隐藏面向管理的组合分析
- countsByStatus 支持 assessorId 过滤;summary 端点对销售按 JWT.uid 强制本人统计 - 看板汇总卡销售看本人(我的评估/被驳回/我的待风控/我的已通过) - 预测准确度/驳回Top/到期/超时 仅风控与管理层展示,销售不再看到跨人数据
This commit is contained in:
@@ -316,13 +316,17 @@ export class PgAssessmentStore implements AssessmentStore {
|
||||
return { items, total };
|
||||
}
|
||||
|
||||
/** 各工作流状态的记录数(用于工作台统计卡)。仅统计未归档项,另返回归档总数。 */
|
||||
async countsByStatus(): Promise<{ total: number; byStatus: Record<string, number>; archived: number }> {
|
||||
/** 各工作流状态的记录数(用于工作台统计卡)。仅统计未归档项,另返回归档总数。可按发起人(assessorId)过滤。 */
|
||||
async countsByStatus(assessorId?: string): Promise<{ total: number; byStatus: Record<string, number>; archived: number }> {
|
||||
const scoped = assessorId !== undefined && assessorId !== '';
|
||||
const cond = scoped ? `AND a.assessment->>'assessorId' = $1` : '';
|
||||
const params = scoped ? [assessorId] : [];
|
||||
const res = await this.pool.query(
|
||||
`SELECT COALESCE(ws.status, 'pending_risk_review') AS status, count(*)::int AS n
|
||||
FROM assessments a LEFT JOIN workflow_status ws ON ws.assessment_id = a.id
|
||||
WHERE a.archived = false
|
||||
WHERE a.archived = false ${cond}
|
||||
GROUP BY 1`,
|
||||
params,
|
||||
);
|
||||
const byStatus: Record<string, number> = {};
|
||||
let total = 0;
|
||||
@@ -331,7 +335,8 @@ export class PgAssessmentStore implements AssessmentStore {
|
||||
total += Number(r.n);
|
||||
}
|
||||
const arc = await this.pool.query(
|
||||
'SELECT count(*)::int AS n FROM assessments WHERE archived = true',
|
||||
`SELECT count(*)::int AS n FROM assessments a WHERE archived = true ${cond}`,
|
||||
params,
|
||||
);
|
||||
const archived = Number((arc.rows[0] as { n: number }).n);
|
||||
return { total, byStatus, archived };
|
||||
|
||||
+6
-1
@@ -1678,13 +1678,18 @@ app.get('/api/assessments', async (c) => {
|
||||
|
||||
/** 工作台统计:各状态评估数。 */
|
||||
app.get('/api/assessments/summary', async (c) => {
|
||||
// 行级隔离:销售只统计本人发起的;风控/管理层全局。
|
||||
let scopeId = c.req.query('assessorId');
|
||||
const authU = (c as import('hono').Context).get('user') as AuthPayload | undefined;
|
||||
if (authU?.role === '商务/销售' && authU.uid) scopeId = authU.uid;
|
||||
if (store instanceof PgAssessmentStore) {
|
||||
return c.json(await store.countsByStatus());
|
||||
return c.json(await store.countsByStatus(scopeId));
|
||||
}
|
||||
const byStatus: Record<string, number> = {};
|
||||
let archivedCount = 0;
|
||||
let activeTotal = 0;
|
||||
for (const r of store.getAll()) {
|
||||
if (scopeId !== undefined && scopeId !== '' && (r.assessment as unknown as { assessorId?: string }).assessorId !== scopeId) continue;
|
||||
if (archivedById.get(r.assessment.id) === true) {
|
||||
archivedCount += 1;
|
||||
continue;
|
||||
|
||||
@@ -465,9 +465,10 @@ export interface AssessmentSummary {
|
||||
readonly archived?: number;
|
||||
}
|
||||
|
||||
/** 获取各状态评估数统计。 */
|
||||
export async function fetchSummary(): Promise<AssessmentSummary> {
|
||||
return request<AssessmentSummary>('GET', '/api/assessments/summary');
|
||||
/** 获取各状态评估数统计。销售传本人 assessorId 则按本人统计(服务端对销售亦强制本人)。 */
|
||||
export async function fetchSummary(assessorId?: string): Promise<AssessmentSummary> {
|
||||
const q = assessorId !== undefined && assessorId !== '' ? `?assessorId=${encodeURIComponent(assessorId)}` : '';
|
||||
return request<AssessmentSummary>('GET', `/api/assessments/summary${q}`);
|
||||
}
|
||||
|
||||
/** 评估详情响应。 */
|
||||
|
||||
+15
-10
@@ -146,7 +146,7 @@ export function Dashboard(): JSX.Element {
|
||||
}, [loadList]);
|
||||
|
||||
const loadAux = useCallback(() => {
|
||||
fetchSummary().then(setSummary).catch(() => undefined);
|
||||
fetchSummary(scopeAssessorId).then(setSummary).catch(() => undefined);
|
||||
const todoStatus = TODO_STATUS[role];
|
||||
if (todoStatus !== undefined) {
|
||||
fetchAssessmentsPage({ page: 1, pageSize: 50, status: todoStatus, ...(scopeAssessorId !== undefined ? { assessorId: scopeAssessorId } : {}) })
|
||||
@@ -155,11 +155,15 @@ export function Dashboard(): JSX.Element {
|
||||
} else {
|
||||
setTodoItems([]);
|
||||
}
|
||||
// 告警数据
|
||||
fetch(`${API_BASE}/api/assessments/expiring`).then((r) => r.json()).then(setExpiring).catch(() => setExpiring([]));
|
||||
fetch(`${API_BASE}/api/assessments/overdue`).then((r) => r.json()).then(setOverdue).catch(() => setOverdue([]));
|
||||
fetch(`${API_BASE}/api/reject-reason-stats`).then((r) => r.json()).then(setRejectStats).catch(() => setRejectStats([]));
|
||||
fetch(`${API_BASE}/api/accuracy`).then((r) => r.json()).then(setAccuracy).catch(() => undefined);
|
||||
// 组合分析(准确度/驳回Top/到期/超时)面向风控与管理层;销售视图不展示(避免跨人数据)。
|
||||
if (role === '商务/销售') {
|
||||
setExpiring([]); setOverdue([]); setRejectStats([]); setAccuracy({ count: 0, avgPredictedPct: null, avgActualPct: null, avgDeviationPct: null, bias: null });
|
||||
} else {
|
||||
fetch(`${API_BASE}/api/assessments/expiring`).then((r) => r.json()).then(setExpiring).catch(() => setExpiring([]));
|
||||
fetch(`${API_BASE}/api/assessments/overdue`).then((r) => r.json()).then(setOverdue).catch(() => setOverdue([]));
|
||||
fetch(`${API_BASE}/api/reject-reason-stats`).then((r) => r.json()).then(setRejectStats).catch(() => setRejectStats([]));
|
||||
fetch(`${API_BASE}/api/accuracy`).then((r) => r.json()).then(setAccuracy).catch(() => undefined);
|
||||
}
|
||||
// 草稿箱(仅销售展示):列出当前用户的向导草稿。
|
||||
if (role === '商务/销售') {
|
||||
listDrafts(user?.id ?? undefined).then(setDrafts).catch(() => setDrafts([]));
|
||||
@@ -318,11 +322,12 @@ export function Dashboard(): JSX.Element {
|
||||
];
|
||||
|
||||
const todoCount = summary.byStatus[TODO_STATUS[role] ?? ''] ?? 0;
|
||||
const isSales = role === '商务/销售';
|
||||
const summaryItems = [
|
||||
{ label: '全部评估', value: summary.total, tone: colorVar('color.brand.primary') },
|
||||
{ label: '我的待办', value: todoCount, tone: colorVar('color.risk.high') },
|
||||
{ label: '待风控', value: summary.byStatus.pending_risk_review ?? 0, tone: colorVar('color.risk.medium') },
|
||||
{ label: '已通过', value: summary.byStatus.approved ?? 0, tone: colorVar('color.risk.low') },
|
||||
{ label: isSales ? '我的评估' : '全部评估', value: summary.total, tone: colorVar('color.brand.primary') },
|
||||
{ label: isSales ? '被驳回(待处理)' : '我的待办', value: todoCount, tone: colorVar('color.risk.high') },
|
||||
{ label: isSales ? '我的待风控' : '待风控', value: summary.byStatus.pending_risk_review ?? 0, tone: colorVar('color.risk.medium') },
|
||||
{ label: isSales ? '我的已通过' : '已通过', value: summary.byStatus.approved ?? 0, tone: colorVar('color.risk.low') },
|
||||
];
|
||||
|
||||
const pageTitle = role === '风控' ? '待办审核' : role === '管理层' ? '待办审批' : '评估历史';
|
||||
|
||||
Reference in New Issue
Block a user