feat(resume): admin bulk resume import
Adds the dashboard surface for the backend's /api/admin/v1/resume/import-batches endpoints, and the multipart support it needed. Client: - rawRequest passes a FormData body through untouched and skips Content-Type, so the browser sets multipart/form-data *with the boundary*. Setting it by hand stripped the boundary and the server saw an empty upload — verified against the backend: 202 with the fix, 422 VALIDATION_ERROR without it. This was the first admin endpoint in the codebase to accept a file. - New api.upload(). fetch reports no upload progress, so upload UI stays indeterminate rather than faking a percentage. UI (/resume/import-batches, gated on admin:resume:read; writes on :manage): - Upload dialog with countryCode and dry-run checked by default — seeing the collision report before creating accounts is almost always what you want. - Detail sheet polls every 2s and stops once the batch is terminal. Clickable count tiles filter the per-resume report; step statuses surface `命中缓存` / `复用结果` so the dedup is visible. - Confirm a dry run, retry failed rows, and resolve a missing contact. IMPORT_IDENTITY_TAKEN is shown inline — a collision is expected input, not an error toast. - progress.total is 0 until unpacking finishes, so both views render 准备中… rather than "0 / 0", which would read as an empty batch. types.gen.ts regenerated from the backend's admin spec. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -738,3 +738,89 @@ export interface ExcProvider {
|
||||
created_at?: string
|
||||
}>
|
||||
}
|
||||
|
||||
// ---------- resume bulk import ----------
|
||||
|
||||
export type ResumeImportBatchStatus =
|
||||
| 'queued'
|
||||
| 'unpacking'
|
||||
| 'processing'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
|
||||
/**
|
||||
* What happened to one resume. Independent of the parse status: a resume can
|
||||
* parse perfectly and still end up `skipped_duplicate`.
|
||||
*/
|
||||
export type ResumeImportOutcome =
|
||||
| 'created'
|
||||
| 'would_create'
|
||||
| 'skipped_duplicate'
|
||||
| 'reused_import'
|
||||
| 'needs_contact'
|
||||
| 'duplicate_file'
|
||||
| 'unsupported_entry'
|
||||
| 'entry_too_large'
|
||||
| 'parse_failed'
|
||||
| 'failed'
|
||||
|
||||
/** `cached` / `reused` mean no provider call was made for that step. */
|
||||
export type ResumeImportStepStatus =
|
||||
| 'pending'
|
||||
| 'processing'
|
||||
| 'completed'
|
||||
| 'cached'
|
||||
| 'reused'
|
||||
| 'skipped'
|
||||
| 'failed'
|
||||
|
||||
export interface ResumeImportBatch {
|
||||
id: string
|
||||
kind: 'resume-import-batch'
|
||||
status: ResumeImportBatchStatus
|
||||
dryRun: boolean
|
||||
progress: {
|
||||
/** 0 until unpacking finishes — render a preparing state, not "0 / 0". */
|
||||
total: number
|
||||
processed: number
|
||||
percent: number
|
||||
}
|
||||
counts: Partial<Record<ResumeImportOutcome, number>>
|
||||
archive: { filename: string | null; size: number | null }
|
||||
options: {
|
||||
countryCode: string | null
|
||||
languageHint: string | null
|
||||
schemaVersion: string | null
|
||||
}
|
||||
error: { code: string; message: string } | null
|
||||
createdBy: string | null
|
||||
pollUrl: string
|
||||
createdAt: string | null
|
||||
startedAt: string | null
|
||||
finishedAt: string | null
|
||||
}
|
||||
|
||||
export interface ResumeImportItem {
|
||||
id: string
|
||||
kind: 'resume-import'
|
||||
status: 'queued' | 'processing' | 'completed' | 'failed'
|
||||
outcome: ResumeImportOutcome | null
|
||||
steps: { extraction: ResumeImportStepStatus; structure: ResumeImportStepStatus }
|
||||
document: {
|
||||
filename: string | null
|
||||
archiveEntry: string | null
|
||||
mimeType: string | null
|
||||
size: number | null
|
||||
extractionMethod: string | null
|
||||
}
|
||||
identityType: 'phone' | 'email' | null
|
||||
matchedUserUid: string | null
|
||||
duplicateOfImportId: string | null
|
||||
error: { code: string; message: string } | null
|
||||
createdAt: string | null
|
||||
finishedAt: string | null
|
||||
// detail-only — never present on the list
|
||||
identityValue?: string | null
|
||||
metrics?: Record<string, number> | null
|
||||
result?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user