feat: add reconciliation and quality reporting

This commit is contained in:
hectorzhao
2026-07-15 14:22:50 +08:00
parent 16311546af
commit 8c3336600e
32 changed files with 1730 additions and 200 deletions
+29 -116
View File
@@ -34,28 +34,11 @@ type DrainageInfo = {
type UploadedFileRef = FileRef;
type ReportValues = Record<string, string | UploadedFileRef | null>;
type SignatureProfile = {
basis: string;
companyName: string;
creditCode: string;
legalPersonName: string;
legalPersonIdCard: string;
responsibleName: string;
responsiblePhone: string;
responsibleIdCard: string;
credentialFile?: UploadedFileRef | null;
legalFrontFile?: UploadedFileRef | null;
legalBackFile?: UploadedFileRef | null;
responsibleFrontFile?: UploadedFileRef | null;
responsibleBackFile?: UploadedFileRef | null;
};
type SignatureFormState = {
tenantId: string;
applicationId: string;
name: string;
purpose: string;
profile: SignatureProfile;
mobile: CarrierStatus;
unicom: CarrierStatus;
telecom: CarrierStatus;
@@ -114,7 +97,7 @@ function readDrainagePayload(signature: ClientSmsSignature) {
unicom: normalizeCarrierStatus(carrierStatus.unicom, fallbackStatus),
telecom: normalizeCarrierStatus(carrierStatus.telecom, fallbackStatus),
},
signatureProfile: normalizeSignatureProfile(profile, signature),
signatureProfile: profile,
signatureReportValues: normalizeReportValues(payload.signatureReportValues),
links: links.map((item) => ({
id: String(item.id ?? `drain-${Date.now()}`),
@@ -142,7 +125,7 @@ function readDrainagePayload(signature: ClientSmsSignature) {
};
}
function buildDrainagePayload(carrierStatus: { mobile: CarrierStatus; unicom: CarrierStatus; telecom: CarrierStatus }, links: DrainageInfo[], signatureProfile?: SignatureProfile, signatureReportValues?: ReportValues) {
function buildDrainagePayload(carrierStatus: { mobile: CarrierStatus; unicom: CarrierStatus; telecom: CarrierStatus }, links: DrainageInfo[], signatureProfile?: Record<string, unknown>, signatureReportValues?: ReportValues) {
return { carrierStatus, links, signatureProfile, signatureReportValues };
}
@@ -164,43 +147,6 @@ function normalizeUploadedFile(value: unknown): UploadedFileRef | null {
return fileObjectId || fileName ? { contentType, fileObjectId, fileName } : null;
}
function emptySignatureProfile(signature?: ClientSmsSignature): SignatureProfile {
return {
basis: '',
companyName: signature?.tenant?.name ?? '',
creditCode: '',
legalPersonName: '',
legalPersonIdCard: '',
responsibleName: '',
responsiblePhone: '',
responsibleIdCard: '',
credentialFile: null,
legalFrontFile: null,
legalBackFile: null,
responsibleFrontFile: null,
responsibleBackFile: null,
};
}
function normalizeSignatureProfile(value: Record<string, unknown>, signature?: ClientSmsSignature): SignatureProfile {
return {
...emptySignatureProfile(signature),
basis: String(value.basis ?? ''),
companyName: String(value.companyName ?? signature?.tenant?.name ?? ''),
creditCode: String(value.creditCode ?? ''),
legalPersonName: String(value.legalPersonName ?? ''),
legalPersonIdCard: String(value.legalPersonIdCard ?? ''),
responsibleName: String(value.responsibleName ?? ''),
responsiblePhone: String(value.responsiblePhone ?? ''),
responsibleIdCard: String(value.responsibleIdCard ?? ''),
credentialFile: normalizeUploadedFile(value.credentialFile),
legalFrontFile: normalizeUploadedFile(value.legalFrontFile),
legalBackFile: normalizeUploadedFile(value.legalBackFile),
responsibleFrontFile: normalizeUploadedFile(value.responsibleFrontFile),
responsibleBackFile: normalizeUploadedFile(value.responsibleBackFile),
};
}
function normalizeCarrierStatus(value: unknown, fallback: CarrierStatus = 'filing'): CarrierStatus {
return value === 'approved' || value === 'pending' || value === 'rejected' || value === 'filing' ? value : fallback;
}
@@ -270,6 +216,7 @@ function DynamicReportFields({ fields, onChange, title, values }: { fields: Appl
const channels = Array.from(new Map(fields.flatMap((field) => field.channels).map((channel) => [channel.id, channel])).values());
const groups = Array.from(new Map(channels.map((channel) => [channel.groupId, channel.groupName])).entries());
const requiredCount = fields.filter((field) => field.required).length;
const commonCount = fields.filter((field) => (field.commonReportTypes?.length ?? 0) > 0).length;
return (
<section>
<div className="report-requirement-heading">
@@ -278,16 +225,19 @@ function DynamicReportFields({ fields, onChange, title, values }: { fields: Appl
</div>
<div className="signature-alert">
<Info size={18} />
<span> {groups.length} {channels.length} {fields.length} {requiredCount} </span>
<span> {commonCount} {groups.length} {channels.length} {fields.length} {requiredCount} </span>
</div>
<div className="signature-form-grid">
{fields.map((field) => {
const channelHint = field.channels.map((channel) => channel.name).join('、');
const requiredChannels = field.required ? field.channels.filter((channel) => channel.required).map((channel) => channel.name).join('、') : '';
const isCommon = (field.commonReportTypes?.length ?? 0) > 0;
const label = `${field.required ? '* ' : ''}${field.name}`;
const hint = field.required
? `${requiredChannels} 要求,至少一个通道配置为必填`
: `适用通道:${channelHint}`;
const hint = isCommon
? `平台通用${field.required ? '必填' : '选填'}资料${channelHint ? `,适用于:${channelHint}` : ''}`
: field.required
? `${requiredChannels} 要求,至少一个通道配置为必填`
: `适用通道:${channelHint}`;
return field.fieldType === 'file' || field.fieldType === 'image' ? (
<div key={field.id}>
<SignatureUploadBox compact file={typeof values[field.code] === 'object' ? values[field.code] as UploadedFileRef : null} label={label} onUploaded={(file) => onChange(field.code, file)} />
@@ -303,7 +253,13 @@ function DynamicReportFields({ fields, onChange, title, values }: { fields: Appl
</div>
<Modal footer={<Button onClick={() => setExplanationOpen(false)}></Button>} onClose={() => setExplanationOpen(false)} open={explanationOpen} size="xl" title="这些资料从哪里来?">
<div className="report-requirement-explanation">
<p> </p>
<p> </p>
{commonCount > 0 ? (
<section className="report-source-group">
<h4></h4>
<ul>{fields.filter((field) => (field.commonReportTypes?.length ?? 0) > 0).map((field) => <li key={field.id}>{field.name} · {field.commonReportTypes?.includes('signature') ? '签名报备' : '引流信息报备'} · {field.required ? '必填' : '选填'}</li>)}</ul>
</section>
) : null}
{groups.map(([groupId, groupName]) => (
<section className="report-source-group" key={groupId}>
<h4>{groupName}</h4>
@@ -344,7 +300,6 @@ function SignatureFormModal({
applicationId: item?.applicationId ?? '',
name: item?.name ?? '',
purpose: item?.purpose ?? '',
profile: payload?.signatureProfile ?? emptySignatureProfile(item),
mobile: payload?.carrierStatus.mobile ?? 'filing',
unicom: payload?.carrierStatus.unicom ?? 'filing',
telecom: payload?.carrierStatus.telecom ?? 'filing',
@@ -354,21 +309,16 @@ function SignatureFormModal({
const tenantApplications = applications.filter((application) => application.tenantId === form.tenantId && application.status !== 'deleted');
useEffect(() => {
if (!form.applicationId) {
setReportFields([]);
return;
}
adminApi.listApplicationReportFields(form.applicationId, 'signature').then(setReportFields).catch(() => setReportFields([]));
const request = form.applicationId
? adminApi.listApplicationReportFields(form.applicationId, 'signature')
: adminApi.listCommonApplicationReportFields('signature');
request.then(setReportFields).catch(() => setReportFields([]));
}, [form.applicationId]);
function update<Key extends keyof SignatureFormState>(key: Key, value: SignatureFormState[Key]) {
setForm((current) => ({ ...current, [key]: value }));
}
function updateProfile<Key extends keyof SignatureProfile>(key: Key, value: SignatureProfile[Key]) {
setForm((current) => ({ ...current, profile: { ...current.profile, [key]: value } }));
}
function updateReportValue(code: string, value: string | UploadedFileRef | null) {
setForm((current) => ({ ...current, reportValues: { ...current.reportValues, [code]: value } }));
}
@@ -419,50 +369,11 @@ function SignatureFormModal({
]}
value={form.applicationId}
/>
<Select
label="* 签名依据"
onChange={(event) => updateProfile('basis', event.target.value)}
options={[
{ label: '请选择签名依据', value: '' },
{ label: '企事业单位证明', value: 'company' },
{ label: '商标注册证', value: 'trademark' },
{ label: '授权委托书', value: 'authorization' },
]}
value={form.profile.basis}
/>
<Input label="* 短信签名" onChange={(event) => update('name', event.target.value)} placeholder="请输入短信签名,如【XXXX公司】" required value={form.name} />
</div>
<SignatureUploadBox
file={form.profile.credentialFile}
label="* 资质凭证"
onUploaded={(file) => updateProfile('credentialFile', file)}
/>
</section>
<section>
<h3></h3>
<div className="signature-form-grid">
<Input label="* 公司名称" onChange={(event) => updateProfile('companyName', event.target.value)} placeholder="请输入公司名称" value={form.profile.companyName} />
<Input label="* 统一社会信用代码" onChange={(event) => updateProfile('creditCode', event.target.value)} placeholder="请输入统一社会信用代码" value={form.profile.creditCode} />
<Input label="* 法人姓名" onChange={(event) => updateProfile('legalPersonName', event.target.value)} placeholder="请输入法人姓名" value={form.profile.legalPersonName} />
<Input label="法人身份证号" onChange={(event) => updateProfile('legalPersonIdCard', event.target.value)} placeholder="请输入法人身份证号" value={form.profile.legalPersonIdCard} />
<SignatureUploadBox compact file={form.profile.legalFrontFile} label="法人身份证照片-人像面" onUploaded={(file) => updateProfile('legalFrontFile', file)} />
<SignatureUploadBox compact file={form.profile.legalBackFile} label="法人身份证照片-国徽面" onUploaded={(file) => updateProfile('legalBackFile', file)} />
</div>
</section>
<section>
<h3></h3>
<div className="signature-form-grid">
<Input label="* 责任人姓名" onChange={(event) => updateProfile('responsibleName', event.target.value)} placeholder="请输入责任人姓名" value={form.profile.responsibleName} />
<Input label="* 责任人手机号" onChange={(event) => updateProfile('responsiblePhone', event.target.value)} placeholder="请输入责任人手机号" value={form.profile.responsiblePhone} />
<Input className="signature-form-grid__wide" label="* 责任人身份证号" onChange={(event) => updateProfile('responsibleIdCard', event.target.value)} placeholder="请输入责任人身份证号" value={form.profile.responsibleIdCard} />
<SignatureUploadBox compact file={form.profile.responsibleFrontFile} label="责任人身份证照片-人像面" onUploaded={(file) => updateProfile('responsibleFrontFile', file)} />
<SignatureUploadBox compact file={form.profile.responsibleBackFile} label="责任人身份证照片-国徽面" onUploaded={(file) => updateProfile('responsibleBackFile', file)} />
</div>
</section>
<DynamicReportFields fields={reportFields} onChange={updateReportValue} title="应用通道签名报备资料" values={form.reportValues} />
<DynamicReportFields fields={reportFields} onChange={updateReportValue} title="签名报备资料(通用 + 通道)" values={form.reportValues} />
</div>
</Modal>
@@ -494,8 +405,10 @@ function DrainageFormModal({ applicationId, item, onClose, onSubmit }: { applica
});
useEffect(() => {
if (!applicationId) return;
adminApi.listApplicationReportFields(applicationId, 'drainage').then(setReportFields).catch(() => setReportFields([]));
const request = applicationId
? adminApi.listApplicationReportFields(applicationId, 'drainage')
: adminApi.listCommonApplicationReportFields('drainage');
request.then(setReportFields).catch(() => setReportFields([]));
}, [applicationId]);
function update<Key extends keyof DrainageInfo>(key: Key, value: DrainageInfo[Key]) {
@@ -541,7 +454,7 @@ function DrainageFormModal({ applicationId, item, onClose, onSubmit }: { applica
<Input label="* 引流信息" onChange={(event) => update('siteName', event.target.value)} placeholder="请输入引流信息" value={form.siteName} />
<Textarea className="signature-form-grid__wide" label="备注" onChange={(event) => update('remark', event.target.value)} rows={4} value={form.remark} />
</div>
<DynamicReportFields fields={reportFields} onChange={updateReportValue} title="应用通道引流信息报备资料" values={form.reportValues} />
<DynamicReportFields fields={reportFields} onChange={updateReportValue} title="引流信息报备资料(通用 + 通道)" values={form.reportValues} />
</section>
</div>
</Modal>
@@ -720,12 +633,12 @@ export function AdminEnterpriseSignaturesPage() {
async function saveSignature(state: SignatureFormState) {
const existing = signatureModal && signatureModal !== 'new' ? signatureModal : null;
const existingPayload = existing ? readDrainagePayload(existing) : { links: [] };
const existingPayload = existing ? readDrainagePayload(existing) : { links: [], signatureProfile: undefined };
const drainageInfo = buildDrainagePayload({
mobile: state.mobile,
unicom: state.unicom,
telecom: state.telecom,
}, existingPayload.links, state.profile, state.reportValues);
}, existingPayload.links, existingPayload.signatureProfile, state.reportValues);
try {
if (existing) {
await adminApi.updateEnterpriseSignature(existing.id, {