feat: refine report material workflow states
This commit is contained in:
@@ -45,7 +45,7 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
const [reportStatusTarget, setReportStatusTarget] = useState<ClientSmsSignature | null>(null);
|
||||
const [signatures, setSignatures] = useState<ClientSmsSignature[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [pendingReportDetailTotal, setPendingReportDetailTotal] = useState(0);
|
||||
const [pendingReportMaterialTotal, setPendingReportMaterialTotal] = useState(0);
|
||||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
@@ -70,7 +70,7 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
if (sequence !== listRequestSequence.current) return;
|
||||
setSignatures(signatureResult.items);
|
||||
setTotal(signatureResult.total);
|
||||
setPendingReportDetailTotal(signatureResult.pendingReportDetailTotal);
|
||||
setPendingReportMaterialTotal(signatureResult.pendingReportMaterialTotal);
|
||||
setError('');
|
||||
} catch (failure) {
|
||||
if (sequence !== listRequestSequence.current) return;
|
||||
@@ -317,11 +317,11 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
</div>
|
||||
|
||||
<div className="enterprise-signature-pending-summary" role="status">
|
||||
<span>待生成批次的签名报备明细</span>
|
||||
<strong>{pendingReportDetailTotal}</strong>
|
||||
<span>条</span>
|
||||
<Button onClick={() => navigate('/admin/report-tasks?scope=pending')} size="sm" variant="ghost">
|
||||
查看明细
|
||||
<span>待生成报备资料</span>
|
||||
<strong>{pendingReportMaterialTotal}</strong>
|
||||
<span>份</span>
|
||||
<Button onClick={() => navigate('/admin/report-materials')} size="sm" variant="ghost">
|
||||
查看详情
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,6 +24,61 @@ import {
|
||||
import { formatDateTime } from '@/utils/dateTime';
|
||||
import { createUuid } from '@/utils/randomId';
|
||||
|
||||
type PoolVisualState = {
|
||||
label: string;
|
||||
tone: 'neutral' | 'success' | 'warning' | 'danger';
|
||||
detail?: string;
|
||||
detailDanger?: boolean;
|
||||
};
|
||||
|
||||
function poolVisualState(
|
||||
materialVersion: number,
|
||||
inspection?: ReportMaterialBatchPreflight['items'][number],
|
||||
): PoolVisualState {
|
||||
if (!inspection) return { label: `V${materialVersion}资格检查中`, tone: 'neutral' };
|
||||
const targets = inspection.targets;
|
||||
const eligibleCount = targets.filter((target) => target.eligible).length;
|
||||
const blockedTargets = targets.filter((target) => !target.eligible);
|
||||
const blockedReasons = [...new Set(blockedTargets.flatMap((target) => target.blockedReasons ?? []))];
|
||||
const allTargetsMatch = (fragment: string) =>
|
||||
targets.length > 0 &&
|
||||
targets.every((target) => (target.blockedReasons ?? []).some((reason) => reason.includes(fragment)));
|
||||
|
||||
if (allTargetsMatch('已放弃报备')) return { label: '全部放弃', tone: 'neutral' };
|
||||
if (
|
||||
targets.length === 0 ||
|
||||
inspection.blockedReasons.some((reason) =>
|
||||
['未绑定短信应用', '短信应用未启用', '当前应用没有启用且可路由的通道'].some((fragment) =>
|
||||
reason.includes(fragment),
|
||||
),
|
||||
)
|
||||
) {
|
||||
return {
|
||||
label: '无有效通道',
|
||||
tone: 'neutral',
|
||||
detail: inspection.blockedReasons[0],
|
||||
};
|
||||
}
|
||||
if (allTargetsMatch('同一资料版本已在批次')) {
|
||||
return { label: `V${materialVersion}已生成`, tone: 'success' };
|
||||
}
|
||||
if (eligibleCount > 0 && blockedTargets.length > 0) {
|
||||
return {
|
||||
label: `V${materialVersion}部分可生成`,
|
||||
tone: 'warning',
|
||||
detail: blockedReasons[0],
|
||||
detailDanger: true,
|
||||
};
|
||||
}
|
||||
if (eligibleCount > 0) return { label: `V${materialVersion}待生成`, tone: 'warning' };
|
||||
return {
|
||||
label: `V${materialVersion}资料不完整`,
|
||||
tone: 'danger',
|
||||
detail: blockedReasons[0] ?? inspection.blockedReasons[0],
|
||||
detailDanger: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function AdminReportMaterialsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [pendingData, setPendingData] = useState<{ items: ReportMaterialPendingItem[]; total: number }>({
|
||||
@@ -187,17 +242,18 @@ export function AdminReportMaterialsPage() {
|
||||
},
|
||||
{
|
||||
key: 'eligibility',
|
||||
title: '版本/资格',
|
||||
title: '版本/状态',
|
||||
render: (item) => {
|
||||
const eligibility = poolEligibility.get(item.id);
|
||||
const eligible = eligibility?.eligible;
|
||||
const visualState = poolVisualState(item.materialVersion, eligibility);
|
||||
return (
|
||||
<div>
|
||||
<Tag tone={eligible ? 'success' : 'warning'}>
|
||||
V{item.materialVersion} ·{' '}
|
||||
{eligible ? `${eligibility.targets.filter((target) => target.eligible).length}个通道可生成` : '待补充'}
|
||||
</Tag>
|
||||
{!eligible ? <div className="muted">{eligibility?.blockedReasons[0] ?? '资格检查中'}</div> : null}
|
||||
<Tag tone={visualState.tone}>{visualState.label}</Tag>
|
||||
{visualState.detail ? (
|
||||
<div className={visualState.detailDanger ? 'muted status-danger' : 'muted'}>
|
||||
{visualState.detail}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -118,6 +118,59 @@ describe('report workbench pages', () => {
|
||||
expect(screen.queryByText('选择本页全部可生成资料')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses explicit colors and text for material pool work states', async () => {
|
||||
const materials = [
|
||||
['pending', '待生成'],
|
||||
['partial', '部分可生成'],
|
||||
['incomplete', '资料不完整'],
|
||||
['abandoned', '全部放弃'],
|
||||
['no-route', '无有效通道'],
|
||||
['generated', '已生成'],
|
||||
].map(([id]) => ({
|
||||
id: `signature:${id}`,
|
||||
tenantId: 'tenant-1',
|
||||
signatureId: `signature-${id}`,
|
||||
applicationId: 'app-1',
|
||||
reportType: 'signature' as const,
|
||||
name: `资料${id}`,
|
||||
materialVersion: 2,
|
||||
changedAt: '2026-09-03T01:00:00.000Z',
|
||||
tenant: { id: 'tenant-1', name: '测试企业' },
|
||||
application: { id: 'app-1', name: '测试应用' },
|
||||
}));
|
||||
const target = (eligible: boolean, blockedReasons: string[] = []) => ({ eligible, blockedReasons });
|
||||
adminApi.listPendingReportMaterials.mockResolvedValue({ items: materials, total: materials.length, page: 1, pageSize: 20 });
|
||||
adminApi.preflightReportMaterialBatch.mockResolvedValue({
|
||||
eligible: true,
|
||||
eligibleTargetCount: 2,
|
||||
skippedTargetCount: 5,
|
||||
items: [
|
||||
{ id: 'signature:pending', eligible: true, blockedReasons: [], targets: [target(true)] },
|
||||
{ id: 'signature:partial', eligible: true, blockedReasons: ['缺少必填字段:营业执照'], targets: [target(true), target(false, ['缺少必填字段:营业执照'])] },
|
||||
{ id: 'signature:incomplete', eligible: false, blockedReasons: ['缺少必填字段:营业执照'], targets: [target(false, ['缺少必填字段:营业执照'])] },
|
||||
{ id: 'signature:abandoned', eligible: false, blockedReasons: ['该通道报备明细已放弃报备'], targets: [target(false, ['该通道报备明细已放弃报备'])] },
|
||||
{ id: 'signature:no-route', eligible: false, blockedReasons: ['当前应用没有启用且可路由的通道'], targets: [] },
|
||||
{ id: 'signature:generated', eligible: false, blockedReasons: ['同一资料版本已在批次 RB-1 生成'], targets: [target(false, ['同一资料版本已在批次 RB-1 生成'])] },
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<AdminReportMaterialsPage />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('V2待生成')).toHaveClass('ui-tag--warning');
|
||||
expect(screen.getByText('V2部分可生成')).toHaveClass('ui-tag--warning');
|
||||
expect(screen.getByText('V2资料不完整')).toHaveClass('ui-tag--danger');
|
||||
expect(screen.getByText('全部放弃')).toHaveClass('ui-tag--neutral');
|
||||
expect(screen.getByText('无有效通道')).toHaveClass('ui-tag--neutral');
|
||||
expect(screen.getByText('V2已生成')).toHaveClass('ui-tag--success');
|
||||
screen.getAllByText('缺少必填字段:营业执照').forEach((message) =>
|
||||
expect(message).toHaveClass('status-danger'),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows and copies the channel brief returned by the batch detail API', async () => {
|
||||
const batch = {
|
||||
id: 'batch-1',
|
||||
|
||||
Reference in New Issue
Block a user