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
+38 -28
View File
@@ -71,8 +71,10 @@ function ClientDrainageModal({ item, onClose, onSaved, signature }: { item?: Cli
const [error, setError] = useState('');
useEffect(() => {
if (!signature.applicationId) return;
clientApi.listApplicationReportFields(signature.applicationId).then(setFields).catch((failure: Error) => setError(failure.message || '引流报备字段加载失败'));
const request = signature.applicationId
? clientApi.listApplicationReportFields(signature.applicationId, 'drainage')
: clientApi.listCommonApplicationReportFields('drainage');
request.then(setFields).catch((failure: Error) => setError(failure.message || '引流报备字段加载失败'));
}, [signature.applicationId]);
async function upload(field: ApplicationReportField, file?: File) {
@@ -101,7 +103,7 @@ function ClientDrainageModal({ item, onClose, onSaved, signature }: { item?: Cli
<Input label="引流信息" onChange={(event) => setSiteName(event.target.value)} required value={siteName} />
<Input label="引流地址" onChange={(event) => setUrl(event.target.value)} placeholder="https://" required value={url} />
<Textarea label="备注" onChange={(event) => setRemark(event.target.value)} rows={3} value={remark} />
<section className="surface" style={{ padding: 16 }}><h3></h3><div className="signature-form-grid" style={{ marginTop: 12 }}>
<section className="surface" style={{ padding: 16 }}><h3> + </h3><div className="signature-form-grid" style={{ marginTop: 12 }}>
{fields.map((field) => field.fieldType === 'string' ? <Input key={field.id} label={`${field.required ? '* ' : ''}${field.name}`} onChange={(event) => setValues((current) => ({ ...current, [field.code]: event.target.value }))} value={String(values[field.code] ?? '')} /> : <label className="signature-upload" key={field.id}><Upload size={28} /><strong>{reportFileRef(values[field.code])?.fileName ?? `${field.required ? '* ' : ''}上传${field.name}`}</strong><small>{uploadingCode === field.code ? '上传中...' : field.fieldType === 'image' ? '请选择图片文件' : '请选择文件'}</small><FileActions file={reportFileRef(values[field.code])} /><input accept={field.fieldType === 'image' ? 'image/*' : undefined} onChange={(event) => void upload(field, event.target.files?.[0])} style={{ display: 'none' }} type="file" /></label>)}
</div></section>
{error ? <p className="form-error">{error}</p> : null}
@@ -119,7 +121,9 @@ export function ClientSignaturesPage() {
const [applicationId, setApplicationId] = useState('');
const [name, setName] = useState('');
const [purpose, setPurpose] = useState('');
const [file, setFile] = useState<File | null>(null);
const [signatureFields, setSignatureFields] = useState<ApplicationReportField[]>([]);
const [signatureValues, setSignatureValues] = useState<Record<string, unknown>>({});
const [signatureUploadingCode, setSignatureUploadingCode] = useState('');
const [page, setPage] = useState(1);
const [drainageModal, setDrainageModal] = useState<{ signature: ClientSmsSignature; item?: ClientDrainageInfo }>();
@@ -139,6 +143,14 @@ export function ClientSignaturesPage() {
loadData();
}, []);
useEffect(() => {
if (!modalOpen) return;
const request = applicationId
? clientApi.listApplicationReportFields(applicationId, 'signature')
: clientApi.listCommonApplicationReportFields('signature');
request.then(setSignatureFields).catch((reason: Error) => setError(reason.message || '签名报备字段加载失败'));
}, [applicationId, modalOpen]);
const filteredSignatures = useMemo(() => signatures.filter((item) => (
!keyword || [item.name, item.purpose, item.applicationId].join(' ').includes(keyword)
)), [keyword, signatures]);
@@ -153,30 +165,33 @@ export function ClientSignaturesPage() {
async function createSignature() {
try {
const signature = await clientApi.createSignature({ applicationId: applicationId || undefined, name, purpose });
if (file) {
const fileObject = await clientApi.uploadFileObject(file, {
purpose: 'signature_material',
prefix: `signature-materials/${signature.id}`,
});
await clientApi.createSignatureMaterial(signature.id, {
fileObjectId: fileObject.id,
materialType: file.type.startsWith('image/') ? 'image' : 'file',
title: file.name,
});
}
const signature = await clientApi.createSignature({ applicationId: applicationId || undefined, name, purpose, drainageInfo: { signatureReportValues: signatureValues } });
await clientApi.submitSignature(signature.id);
setModalOpen(false);
setApplicationId('');
setName('');
setPurpose('');
setFile(null);
setSignatureFields([]);
setSignatureValues({});
loadData();
} catch (reason) {
setError(reason instanceof Error ? reason.message : '签名提交失败');
}
}
async function uploadSignatureField(field: ApplicationReportField, file?: File) {
if (!file) return;
setSignatureUploadingCode(field.code);
try {
const uploaded = await clientApi.uploadFileObject(file, { purpose: 'signature_report_material', prefix: 'signature-materials' });
setSignatureValues((current) => ({ ...current, [field.code]: { fileObjectId: uploaded.id, fileName: uploaded.fileName, contentType: uploaded.contentType } }));
} catch (reason) {
setError(reason instanceof Error ? reason.message : '签名报备资料上传失败');
} finally {
setSignatureUploadingCode('');
}
}
function disableSignature(id: string) {
clientApi.changeSignatureStatus(id, 'disabled')
.then(loadData)
@@ -260,7 +275,7 @@ export function ClientSignaturesPage() {
footer={(
<>
<Button onClick={() => setModalOpen(false)} variant="ghost"></Button>
<Button disabled={!name} onClick={createSignature}></Button>
<Button disabled={!name || signatureFields.some((field) => field.required && !signatureValues[field.code]) || Boolean(signatureUploadingCode)} onClick={createSignature}></Button>
</>
)}
onClose={() => setModalOpen(false)}
@@ -277,16 +292,11 @@ export function ClientSignaturesPage() {
/>
<Input label="短信签名" onChange={(event) => setName(event.target.value)} placeholder="请输入短信签名,如【某某科技】" value={name} />
<Input label="用途" onChange={(event) => setPurpose(event.target.value)} placeholder="请输入签名用途" value={purpose} />
<label className="signature-upload">
<Upload size={36} />
<strong>{file ? file.name : '上传资质图片或文件'}</strong>
<small>PDFWord </small>
<input
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
style={{ display: 'none' }}
type="file"
/>
</label>
<section className="surface" style={{ padding: 16 }}><h3></h3><div className="signature-form-grid" style={{ marginTop: 12 }}>
{signatureFields.map((field) => field.fieldType === 'string'
? <Input key={field.id} label={`${field.required ? '* ' : ''}${field.name}`} onChange={(event) => setSignatureValues((current) => ({ ...current, [field.code]: event.target.value }))} value={String(signatureValues[field.code] ?? '')} />
: <label className="signature-upload" key={field.id}><Upload size={28} /><strong>{reportFileRef(signatureValues[field.code])?.fileName ?? `${field.required ? '* ' : ''}上传${field.name}`}</strong><small>{signatureUploadingCode === field.code ? '上传中...' : field.fieldType === 'image' ? '请选择图片文件' : '请选择文件'}</small><FileActions file={reportFileRef(signatureValues[field.code])} /><input accept={field.fieldType === 'image' ? 'image/*' : undefined} onChange={(event) => void uploadSignatureField(field, event.target.files?.[0])} style={{ display: 'none' }} type="file" /></label>)}
</div></section>
</div>
</Modal>
{drainageModal ? <ClientDrainageModal item={drainageModal.item} onClose={() => setDrainageModal(undefined)} onSaved={() => { setDrainageModal(undefined); loadData(); }} signature={drainageModal.signature} /> : null}