feat: complete drainage review and admin search workflows
This commit is contained in:
@@ -27,6 +27,8 @@ type DrainageInfo = {
|
||||
submittedAt: string;
|
||||
remark: string;
|
||||
reportValues: ReportValues;
|
||||
auditStatus?: string;
|
||||
rejectReason?: string | null;
|
||||
};
|
||||
|
||||
type UploadedFileRef = FileRef;
|
||||
@@ -134,6 +136,8 @@ function readDrainagePayload(signature: ClientSmsSignature) {
|
||||
submittedAt: String(item.submittedAt ?? ''),
|
||||
remark: String(item.remark ?? ''),
|
||||
reportValues: normalizeReportValues(item.reportValues),
|
||||
auditStatus: String(item.auditStatus ?? 'pending'),
|
||||
rejectReason: item.rejectReason ? String(item.rejectReason) : null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -582,7 +586,7 @@ function ChannelReportStatusModal({ item, onClose, onSaved }: { item: ClientSmsS
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
try {
|
||||
await adminApi.changeReportTaskStatuses({ items: targets.map((target) => ({ signatureId: item.id, channelId: target.channelId, status: statuses[target.channelId] ?? target.status })), reason });
|
||||
await adminApi.changeReportTaskStatuses({ items: targets.map((target) => ({ signatureId: item.id, channelId: target.channelId, status: statuses[target.channelId] ?? target.status })), reason, sourceEntry: 'enterprise_signature' });
|
||||
onSaved();
|
||||
} catch (failure) { setError(failure instanceof Error ? failure.message : '报备状态保存失败'); } finally { setSaving(false); }
|
||||
}
|
||||
@@ -623,7 +627,7 @@ function DrainageReportStatusModal({ item, onClose, onSaved, signature }: { item
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
try {
|
||||
await adminApi.changeReportTaskStatuses({ items: targets.map((target) => ({ signatureId: signature.id, channelId: target.channelId, reportType: 'drainage', drainageItemId: item.id, status: statuses[target.channelId] ?? target.status })), reason });
|
||||
await adminApi.changeReportTaskStatuses({ items: targets.map((target) => ({ signatureId: signature.id, channelId: target.channelId, reportType: 'drainage', drainageItemId: item.id, status: statuses[target.channelId] ?? target.status })), reason, sourceEntry: 'enterprise_signature' });
|
||||
onSaved();
|
||||
} catch (failure) { setError(failure instanceof Error ? failure.message : '引流报备状态保存失败'); } finally { setSaving(false); }
|
||||
}
|
||||
@@ -663,6 +667,10 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
const [drainageStatusTarget, setDrainageStatusTarget] = useState<{ signature: ClientSmsSignature; item: DrainageInfo } | null>(null);
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [appliedEnterpriseKeyword, setAppliedEnterpriseKeyword] = useState('');
|
||||
const [applicationKeyword, setApplicationKeyword] = useState('');
|
||||
const [appliedApplicationKeyword, setAppliedApplicationKeyword] = useState('');
|
||||
const [drainageKeyword, setDrainageKeyword] = useState('');
|
||||
const [appliedDrainageKeyword, setAppliedDrainageKeyword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [expandedSignatureId, setExpandedSignatureId] = useState('');
|
||||
const [signatureKeyword, setSignatureKeyword] = useState('');
|
||||
@@ -674,10 +682,10 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
const [tenants, setTenants] = useState<TenantOption[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, signatureKeyword: appliedSignatureKeyword }) {
|
||||
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, signatureKeyword: appliedSignatureKeyword, drainageKeyword: appliedDrainageKeyword }) {
|
||||
try {
|
||||
const [signatureItems, tenantItems, applicationItems] = await Promise.all([
|
||||
adminApi.listEnterpriseSignatures({ keyword: [filters.enterpriseKeyword, filters.signatureKeyword].filter(Boolean).join(' ') }),
|
||||
adminApi.listEnterpriseSignatures(filters),
|
||||
adminApi.listTenants(),
|
||||
adminApi.listEnterpriseApplications(),
|
||||
]);
|
||||
@@ -697,9 +705,12 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
const filteredSignatures = useMemo(() => signatures.filter((item) => {
|
||||
const enterprise = item.tenant?.name ?? item.tenantId;
|
||||
const application = item.application?.name ?? '';
|
||||
const drainageItems = readDrainagePayload(item).links;
|
||||
return (!appliedEnterpriseKeyword || enterprise.includes(appliedEnterpriseKeyword))
|
||||
&& (!appliedSignatureKeyword || item.name.includes(appliedSignatureKeyword) || application.includes(appliedSignatureKeyword) || (item.purpose ?? '').includes(appliedSignatureKeyword));
|
||||
}), [appliedEnterpriseKeyword, appliedSignatureKeyword, signatures]);
|
||||
&& (!appliedApplicationKeyword || application.includes(appliedApplicationKeyword))
|
||||
&& (!appliedSignatureKeyword || item.name.includes(appliedSignatureKeyword) || (item.purpose ?? '').includes(appliedSignatureKeyword))
|
||||
&& (!appliedDrainageKeyword || drainageItems.some((drainage) => `${drainage.siteName} ${drainage.url} ${drainage.remark}`.includes(appliedDrainageKeyword)));
|
||||
}), [appliedApplicationKeyword, appliedDrainageKeyword, appliedEnterpriseKeyword, appliedSignatureKeyword, signatures]);
|
||||
const pageSize = 10;
|
||||
const totalPages = Math.max(1, Math.ceil(filteredSignatures.length / pageSize));
|
||||
const currentPage = Math.min(page, totalPages);
|
||||
@@ -707,7 +718,7 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
}, [appliedEnterpriseKeyword, appliedSignatureKeyword, filteredSignatures.length]);
|
||||
}, [appliedApplicationKeyword, appliedDrainageKeyword, appliedEnterpriseKeyword, appliedSignatureKeyword, filteredSignatures.length]);
|
||||
|
||||
async function saveSignature(state: SignatureFormState) {
|
||||
const existing = signatureModal && signatureModal !== 'new' ? signatureModal : null;
|
||||
@@ -746,13 +757,10 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
if (!signature) {
|
||||
return;
|
||||
}
|
||||
const payload = readDrainagePayload(signature);
|
||||
const links = payload.links.some((current) => current.id === item.id)
|
||||
? payload.links.map((current) => current.id === item.id ? item : current)
|
||||
: [item, ...payload.links];
|
||||
await adminApi.updateEnterpriseSignature(signatureId, {
|
||||
drainageInfo: buildDrainagePayload(payload.carrierStatus, links, payload.signatureProfile, payload.signatureReportValues),
|
||||
});
|
||||
const exists = readDrainagePayload(signature).links.some((current) => current.id === item.id);
|
||||
const body = { siteName: item.siteName, url: item.url, remark: item.remark, reportValues: item.reportValues };
|
||||
if (exists) await adminApi.updateDrainageInfo(item.id, body);
|
||||
else await adminApi.createDrainageInfo(signatureId, body);
|
||||
setDrainageModal(null);
|
||||
setExpandedSignatureId(signatureId);
|
||||
await loadData();
|
||||
@@ -765,13 +773,7 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
if (deleteTarget.kind === 'signature') {
|
||||
await adminApi.changeEnterpriseSignatureStatus(deleteTarget.id, 'deleted', '运营端删除签名');
|
||||
} else {
|
||||
const signature = signatures.find((item) => item.id === deleteTarget.signatureId);
|
||||
if (signature) {
|
||||
const payload = readDrainagePayload(signature);
|
||||
await adminApi.updateEnterpriseSignature(signature.id, {
|
||||
drainageInfo: buildDrainagePayload(payload.carrierStatus, payload.links.filter((item) => item.id !== deleteTarget.id), payload.signatureProfile, payload.signatureReportValues),
|
||||
});
|
||||
}
|
||||
await adminApi.changeDrainageInfoStatus(deleteTarget.id, 'deleted', '运营端删除引流信息');
|
||||
}
|
||||
setDeleteTarget(null);
|
||||
await loadData();
|
||||
@@ -781,9 +783,12 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
<div className="signature-list admin-enterprise-signature-list">
|
||||
{visibleSignatures.map((signature) => {
|
||||
const payload = readDrainagePayload(signature);
|
||||
const visibleDrainageLinks = appliedDrainageKeyword
|
||||
? payload.links.filter((item) => `${item.siteName} ${item.url} ${item.remark}`.includes(appliedDrainageKeyword))
|
||||
: payload.links;
|
||||
const summaryStatuses = Object.values(signature.carrierReportSummary ?? {}).map((summary) => summary.status);
|
||||
const cardTone = summaryStatuses.includes('failed') ? 'red' : summaryStatuses.length > 0 && summaryStatuses.every((status) => status === 'approved' || status === 'not_applicable') ? 'green' : summaryStatuses.some((status) => status === 'reporting') ? 'blue' : 'gray';
|
||||
const expanded = expandedSignatureId === signature.id;
|
||||
const expanded = expandedSignatureId === signature.id || Boolean(appliedDrainageKeyword);
|
||||
return (
|
||||
<article className={`signature-card signature-card--${cardTone}`} key={signature.id}>
|
||||
<div className="signature-summary">
|
||||
@@ -808,30 +813,32 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
{expanded ? (
|
||||
<div className="drainage-panel">
|
||||
<h2>引流信息列表</h2>
|
||||
{payload.links.length ? (
|
||||
{visibleDrainageLinks.length ? (
|
||||
<div className="drainage-table">
|
||||
<div className="drainage-table__head">
|
||||
<span>站名称</span>
|
||||
<span>引流信息</span>
|
||||
<span>审核状态</span>
|
||||
<span>移动</span>
|
||||
<span>联通</span>
|
||||
<span>电信</span>
|
||||
<span>提交时间</span>
|
||||
<span>操作</span>
|
||||
</div>
|
||||
{payload.links.map((item) => {
|
||||
{visibleDrainageLinks.map((item) => {
|
||||
const summary = signature.drainageCarrierReportSummary?.[item.id];
|
||||
return (
|
||||
<div className="drainage-table__row" key={item.id}>
|
||||
<strong>{item.siteName}</strong>
|
||||
<span className="drainage-table__url" title={item.url}>{item.url}</span>
|
||||
<AuditStatusTag status={item.auditStatus ?? 'pending'} />
|
||||
<CarrierReportTag summary={summary?.mobile} />
|
||||
<CarrierReportTag summary={summary?.unicom} />
|
||||
<CarrierReportTag summary={summary?.telecom} />
|
||||
<span className="muted">{item.submittedAt}</span>
|
||||
<span className="drainage-row-actions">
|
||||
<Button onClick={() => setDrainageReport({ signature, item })} size="sm" variant="ghost">报备详情</Button>
|
||||
<Button onClick={() => setDrainageStatusTarget({ signature, item })} size="sm" variant="ghost">报备状态</Button>
|
||||
<Button disabled={item.auditStatus !== 'approved'} onClick={() => setDrainageStatusTarget({ signature, item })} size="sm" variant="ghost">报备状态</Button>
|
||||
<Button onClick={() => setDrainageModal({ signatureId: signature.id, item })} size="sm" variant="ghost">编辑</Button>
|
||||
<Button onClick={() => setDeleteTarget({ kind: 'drainage', signatureId: signature.id, id: item.id, name: item.siteName })} size="sm" variant="danger">删除</Button>
|
||||
</span>
|
||||
@@ -875,20 +882,28 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
|
||||
<div className="surface admin-split-filter">
|
||||
<Input label="企业名称" onChange={(event) => setEnterpriseKeyword(event.target.value)} placeholder="请输入企业名称" prefix={<Search size={16} />} value={enterpriseKeyword} />
|
||||
<Input label="签名/应用" onChange={(event) => setSignatureKeyword(event.target.value)} placeholder="请输入签名或应用名称" prefix={<Search size={16} />} value={signatureKeyword} />
|
||||
<Input label="企业应用" onChange={(event) => setApplicationKeyword(event.target.value)} placeholder="请输入企业应用名称" prefix={<Search size={16} />} value={applicationKeyword} />
|
||||
<Input label="签名名称" onChange={(event) => setSignatureKeyword(event.target.value)} placeholder="请输入签名名称或用途" prefix={<Search size={16} />} value={signatureKeyword} />
|
||||
<Input label="引流信息" onChange={(event) => setDrainageKeyword(event.target.value)} placeholder="请输入站名称、URL 或备注" prefix={<Search size={16} />} value={drainageKeyword} />
|
||||
<div className="admin-split-filter__actions">
|
||||
<Button icon={<Search size={16} />} onClick={() => {
|
||||
const filters = { enterpriseKeyword: enterpriseKeyword.trim(), signatureKeyword: signatureKeyword.trim() };
|
||||
const filters = { enterpriseKeyword: enterpriseKeyword.trim(), applicationKeyword: applicationKeyword.trim(), signatureKeyword: signatureKeyword.trim(), drainageKeyword: drainageKeyword.trim() };
|
||||
setAppliedEnterpriseKeyword(filters.enterpriseKeyword);
|
||||
setAppliedApplicationKeyword(filters.applicationKeyword);
|
||||
setAppliedSignatureKeyword(filters.signatureKeyword);
|
||||
setAppliedDrainageKeyword(filters.drainageKeyword);
|
||||
void loadData(filters);
|
||||
}}>查询</Button>
|
||||
<Button onClick={() => {
|
||||
const filters = { enterpriseKeyword: '', signatureKeyword: '' };
|
||||
const filters = { enterpriseKeyword: '', applicationKeyword: '', signatureKeyword: '', drainageKeyword: '' };
|
||||
setEnterpriseKeyword('');
|
||||
setApplicationKeyword('');
|
||||
setSignatureKeyword('');
|
||||
setDrainageKeyword('');
|
||||
setAppliedEnterpriseKeyword('');
|
||||
setAppliedApplicationKeyword('');
|
||||
setAppliedSignatureKeyword('');
|
||||
setAppliedDrainageKeyword('');
|
||||
void loadData(filters);
|
||||
}} variant="ghost">重置</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user