feat: add reconciliation and quality reporting
This commit is contained in:
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user