feat: build reporting workbench workflow
This commit is contained in:
@@ -1,22 +1,36 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FileSpreadsheet, Plus, Search } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { adminApi, type ClientSmsApplication, type ClientSmsSignature, type TenantOption } from '@/api/adminApi';
|
||||
import { Breadcrumb, Button, Input, Tabs } from '@/components/ui';
|
||||
import { Breadcrumb, Button, Input, Modal, Tabs } from '@/components/ui';
|
||||
import { ReportMaterialImportModal } from './ReportMaterialImportModal';
|
||||
import { DrainageFormModal } from './enterprise-signatures/DrainageFormModal';
|
||||
import { EnterpriseSignaturesTable } from './enterprise-signatures/EnterpriseSignaturesTable';
|
||||
import { SignatureFormModal } from './enterprise-signatures/SignatureFormModal';
|
||||
import { ChannelReportStatusModal, ConfirmModal, DrainageReportStatusModal } from './enterprise-signatures/SignatureReportModals';
|
||||
import {
|
||||
ChannelReportStatusModal,
|
||||
ConfirmModal,
|
||||
DrainageReportStatusModal,
|
||||
} from './enterprise-signatures/SignatureReportModals';
|
||||
import { buildDrainagePayload, readDrainagePayload } from './enterprise-signatures/signature.helpers';
|
||||
import type { DrainageInfo, SignatureFormState } from './enterprise-signatures/signature.types';
|
||||
|
||||
/** R4 page container: owns query state and coordinates focused presentation components. */
|
||||
export function AdminEnterpriseSignaturesPage() {
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState<'sms' | 'mms'>('sms');
|
||||
const [applications, setApplications] = useState<ClientSmsApplication[]>([]);
|
||||
const [deleteTarget, setDeleteTarget] = useState<{ kind: 'drainage'; signatureId: string; id: string; name: string } | null>(null);
|
||||
const [deleteTarget, setDeleteTarget] = useState<{
|
||||
kind: 'drainage';
|
||||
signatureId: string;
|
||||
id: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
const [drainageModal, setDrainageModal] = useState<{ signatureId: string; item?: DrainageInfo } | null>(null);
|
||||
const [drainageStatusTarget, setDrainageStatusTarget] = useState<{ signature: ClientSmsSignature; item: DrainageInfo } | null>(null);
|
||||
const [drainageStatusTarget, setDrainageStatusTarget] = useState<{
|
||||
signature: ClientSmsSignature;
|
||||
item: DrainageInfo;
|
||||
} | null>(null);
|
||||
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
|
||||
const [appliedEnterpriseKeyword, setAppliedEnterpriseKeyword] = useState('');
|
||||
const [applicationKeyword, setApplicationKeyword] = useState('');
|
||||
@@ -36,10 +50,20 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [materialChangedSignature, setMaterialChangedSignature] = useState<ClientSmsSignature | null>(null);
|
||||
|
||||
const pageSize = 10;
|
||||
|
||||
async function loadData(filters = { enterpriseKeyword: appliedEnterpriseKeyword, applicationKeyword: appliedApplicationKeyword, signatureKeyword: appliedSignatureKeyword, drainageKeyword: appliedDrainageKeyword }, targetPage = page, targetSort = signatureSort) {
|
||||
async function loadData(
|
||||
filters = {
|
||||
enterpriseKeyword: appliedEnterpriseKeyword,
|
||||
applicationKeyword: appliedApplicationKeyword,
|
||||
signatureKeyword: appliedSignatureKeyword,
|
||||
drainageKeyword: appliedDrainageKeyword,
|
||||
},
|
||||
targetPage = page,
|
||||
targetSort = signatureSort,
|
||||
) {
|
||||
try {
|
||||
const [signatureResult, tenantItems, applicationItems] = await Promise.all([
|
||||
adminApi.listEnterpriseSignaturesPage({ ...filters, signatureSort: targetSort, page: targetPage, pageSize }),
|
||||
@@ -57,7 +81,7 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void loadData(undefined, page);
|
||||
queueMicrotask(() => void loadData(undefined, page));
|
||||
}, [page]);
|
||||
|
||||
const filteredSignatures = signatures;
|
||||
@@ -68,21 +92,27 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
async function saveSignature(state: SignatureFormState) {
|
||||
const existing = signatureModal && signatureModal !== 'new' ? signatureModal : null;
|
||||
const existingPayload = existing ? readDrainagePayload(existing) : { links: [], signatureProfile: undefined };
|
||||
const drainageInfo = buildDrainagePayload({
|
||||
mobile: state.mobile,
|
||||
unicom: state.unicom,
|
||||
telecom: state.telecom,
|
||||
}, existingPayload.links, existingPayload.signatureProfile, state.reportValues);
|
||||
const drainageInfo = buildDrainagePayload(
|
||||
{
|
||||
mobile: state.mobile,
|
||||
unicom: state.unicom,
|
||||
telecom: state.telecom,
|
||||
},
|
||||
existingPayload.links,
|
||||
existingPayload.signatureProfile,
|
||||
state.reportValues,
|
||||
);
|
||||
try {
|
||||
let saved: ClientSmsSignature;
|
||||
if (existing) {
|
||||
await adminApi.updateEnterpriseSignature(existing.id, {
|
||||
saved = await adminApi.updateEnterpriseSignature(existing.id, {
|
||||
applicationId: state.applicationId || null,
|
||||
drainageInfo,
|
||||
name: state.name,
|
||||
purpose: state.purpose,
|
||||
});
|
||||
} else {
|
||||
await adminApi.createEnterpriseSignature({
|
||||
saved = await adminApi.createEnterpriseSignature({
|
||||
applicationId: state.applicationId || undefined,
|
||||
drainageInfo,
|
||||
name: state.name,
|
||||
@@ -91,6 +121,7 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
});
|
||||
}
|
||||
setSignatureModal(null);
|
||||
if (saved.reportMaterialChanged) setMaterialChangedSignature(saved);
|
||||
await loadData();
|
||||
} catch (failure) {
|
||||
setError(failure instanceof Error ? failure.message : '企业签名保存失败');
|
||||
@@ -154,39 +185,87 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
<h1>企业签名管理</h1>
|
||||
</div>
|
||||
<div className="page-heading-actions">
|
||||
<Button icon={<FileSpreadsheet size={16} />} onClick={() => setImportOpen(true)} variant="ghost">批量导入签名及引流资料</Button>
|
||||
<Button icon={<Plus size={16} />} onClick={() => setSignatureModal(activeTab === 'sms' ? 'new' : null)}>添加签名</Button>
|
||||
<Button icon={<FileSpreadsheet size={16} />} onClick={() => setImportOpen(true)} variant="ghost">
|
||||
批量导入签名及引流资料
|
||||
</Button>
|
||||
<Button icon={<Plus size={16} />} onClick={() => setSignatureModal(activeTab === 'sms' ? 'new' : null)}>
|
||||
添加签名
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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) => 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} />
|
||||
<Input
|
||||
label="企业名称"
|
||||
onChange={(event) => setEnterpriseKeyword(event.target.value)}
|
||||
placeholder="请输入企业名称"
|
||||
prefix={<Search size={16} />}
|
||||
value={enterpriseKeyword}
|
||||
/>
|
||||
<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(), applicationKeyword: applicationKeyword.trim(), signatureKeyword: signatureKeyword.trim(), drainageKeyword: drainageKeyword.trim() };
|
||||
setAppliedEnterpriseKeyword(filters.enterpriseKeyword);
|
||||
setAppliedApplicationKeyword(filters.applicationKeyword);
|
||||
setAppliedSignatureKeyword(filters.signatureKeyword);
|
||||
setAppliedDrainageKeyword(filters.drainageKeyword);
|
||||
setPage(1);
|
||||
void loadData(filters, 1);
|
||||
}}>查询</Button>
|
||||
<Button onClick={() => {
|
||||
const filters = { enterpriseKeyword: '', applicationKeyword: '', signatureKeyword: '', drainageKeyword: '' };
|
||||
setEnterpriseKeyword('');
|
||||
setApplicationKeyword('');
|
||||
setSignatureKeyword('');
|
||||
setDrainageKeyword('');
|
||||
setAppliedEnterpriseKeyword('');
|
||||
setAppliedApplicationKeyword('');
|
||||
setAppliedSignatureKeyword('');
|
||||
setAppliedDrainageKeyword('');
|
||||
setPage(1);
|
||||
void loadData(filters, 1);
|
||||
}} variant="ghost">重置</Button>
|
||||
<Button
|
||||
icon={<Search size={16} />}
|
||||
onClick={() => {
|
||||
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);
|
||||
setPage(1);
|
||||
void loadData(filters, 1);
|
||||
}}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const filters = {
|
||||
enterpriseKeyword: '',
|
||||
applicationKeyword: '',
|
||||
signatureKeyword: '',
|
||||
drainageKeyword: '',
|
||||
};
|
||||
setEnterpriseKeyword('');
|
||||
setApplicationKeyword('');
|
||||
setSignatureKeyword('');
|
||||
setDrainageKeyword('');
|
||||
setAppliedEnterpriseKeyword('');
|
||||
setAppliedApplicationKeyword('');
|
||||
setAppliedSignatureKeyword('');
|
||||
setAppliedDrainageKeyword('');
|
||||
setPage(1);
|
||||
void loadData(filters, 1);
|
||||
}}
|
||||
variant="ghost"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -199,7 +278,12 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
value={activeTab}
|
||||
items={[
|
||||
{ label: '短信签名', value: 'sms', content: smsSignatureContent },
|
||||
{ label: '彩信签名', pending: true, value: 'mms', content: <div className="ui-table__empty">彩信签名待后端能力确认,本页不展示演示数据。</div> },
|
||||
{
|
||||
label: '彩信签名',
|
||||
pending: true,
|
||||
value: 'mms',
|
||||
content: <div className="ui-table__empty">彩信签名待后端能力确认,本页不展示演示数据。</div>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -209,29 +293,87 @@ export function AdminEnterpriseSignaturesPage() {
|
||||
applications={applications}
|
||||
item={signatureModal === 'new' ? undefined : signatureModal}
|
||||
onClose={() => setSignatureModal(null)}
|
||||
onSubmit={(state) => { void saveSignature(state); }}
|
||||
onSubmit={(state) => {
|
||||
void saveSignature(state);
|
||||
}}
|
||||
tenants={tenants}
|
||||
/>
|
||||
) : null}
|
||||
{importOpen ? <ReportMaterialImportModal onClose={() => setImportOpen(false)} onCompleted={() => {
|
||||
setMessage('导入解析完成,合格资料已进入审核中心的导入批次');
|
||||
void loadData();
|
||||
}} /> : null}
|
||||
{reportStatusTarget ? <ChannelReportStatusModal item={reportStatusTarget} onClose={() => setReportStatusTarget(null)} onSaved={() => { setReportStatusTarget(null); void loadData(); }} /> : null}
|
||||
{materialChangedSignature ? (
|
||||
<Modal
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={() => setMaterialChangedSignature(null)} variant="ghost">
|
||||
稍后处理
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const id = materialChangedSignature.id;
|
||||
setMaterialChangedSignature(null);
|
||||
navigate(`/admin/report-materials?signatureId=${encodeURIComponent(id)}`);
|
||||
}}
|
||||
>
|
||||
前往报备资料池
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
onClose={() => setMaterialChangedSignature(null)}
|
||||
open
|
||||
title="签名资料已更新"
|
||||
>
|
||||
<div className="signature-alert">
|
||||
<FileSpreadsheet size={20} />
|
||||
<span>资料发生变化,如需提交至通道报备,请到“报备工作台-报备资料池”生成报备批次。</span>
|
||||
</div>
|
||||
</Modal>
|
||||
) : null}
|
||||
{importOpen ? (
|
||||
<ReportMaterialImportModal
|
||||
onClose={() => setImportOpen(false)}
|
||||
onCompleted={() => {
|
||||
setMessage('导入解析完成,合格资料已进入审核中心的导入批次');
|
||||
void loadData();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{reportStatusTarget ? (
|
||||
<ChannelReportStatusModal
|
||||
item={reportStatusTarget}
|
||||
onClose={() => setReportStatusTarget(null)}
|
||||
onSaved={() => {
|
||||
setReportStatusTarget(null);
|
||||
void loadData();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{drainageModal ? (
|
||||
<DrainageFormModal
|
||||
applicationId={signatures.find((item) => item.id === drainageModal.signatureId)?.applicationId}
|
||||
item={drainageModal.item}
|
||||
onClose={() => setDrainageModal(null)}
|
||||
onSubmit={(item) => { void saveDrainage(drainageModal.signatureId, item); }}
|
||||
onSubmit={(item) => {
|
||||
void saveDrainage(drainageModal.signatureId, item);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{drainageStatusTarget ? (
|
||||
<DrainageReportStatusModal
|
||||
item={drainageStatusTarget.item}
|
||||
onClose={() => setDrainageStatusTarget(null)}
|
||||
onSaved={() => {
|
||||
setDrainageStatusTarget(null);
|
||||
void loadData();
|
||||
}}
|
||||
signature={drainageStatusTarget.signature}
|
||||
/>
|
||||
) : null}
|
||||
{drainageStatusTarget ? <DrainageReportStatusModal item={drainageStatusTarget.item} onClose={() => setDrainageStatusTarget(null)} onSaved={() => { setDrainageStatusTarget(null); void loadData(); }} signature={drainageStatusTarget.signature} /> : null}
|
||||
{deleteTarget ? (
|
||||
<ConfirmModal
|
||||
message={`确认删除“${deleteTarget.name}”吗?删除后会写入真实后台。`}
|
||||
onCancel={() => setDeleteTarget(null)}
|
||||
onConfirm={() => { void confirmDelete(); }}
|
||||
onConfirm={() => {
|
||||
void confirmDelete();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user