feat(import): RennmausPro-III-Backup-Importer (analyze+execute, Dedup, Fotos)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
103
gerbil-manager-web/src/api/rpro3.ts
Normal file
103
gerbil-manager-web/src/api/rpro3.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* RPRO3: API-Client für den RennmausPro-III-Backup-Import.
|
||||
* POST /import/rpro3/analyze -> multipart 'backup' (+ optional 'images') -> Auswertung (kein Schreiben)
|
||||
* POST /import/rpro3/execute -> derselbe Upload -> idempotenter Import
|
||||
*
|
||||
* Upload via FormData (nicht api.post — das erzwingt application/json); der Browser setzt
|
||||
* den multipart-Boundary-Header selbst.
|
||||
*/
|
||||
import { API_BASE_URL, ApiError } from './client'
|
||||
import { de } from '../strings/de'
|
||||
|
||||
export interface Rpro3Counts {
|
||||
ownAnimals: number
|
||||
externalRaw: number
|
||||
externalAfterDedup: number
|
||||
litters: number
|
||||
contacts: number
|
||||
genotypes: number
|
||||
duplicatesMerged: number
|
||||
mergeClusters: number
|
||||
}
|
||||
|
||||
export interface Rpro3AmbiguousVariant {
|
||||
count: number
|
||||
dob: string
|
||||
farbe: string
|
||||
origin: string
|
||||
isOwn: boolean
|
||||
}
|
||||
|
||||
export interface Rpro3AmbiguousName {
|
||||
name: string
|
||||
variants: Rpro3AmbiguousVariant[]
|
||||
bareCount: number
|
||||
}
|
||||
|
||||
export interface Rpro3MergeSample {
|
||||
name: string
|
||||
recordCount: number
|
||||
dob: string
|
||||
farbe: string
|
||||
origin: string
|
||||
}
|
||||
|
||||
export interface Rpro3AnalyzeResult {
|
||||
counts: Rpro3Counts
|
||||
newVsCurrentNew: number
|
||||
newVsCurrentExisting: number
|
||||
topMerges: Rpro3MergeSample[]
|
||||
ambiguousNames: Rpro3AmbiguousName[]
|
||||
photosProvided: boolean
|
||||
photoFilesAvailable: number
|
||||
}
|
||||
|
||||
export interface Rpro3ExecuteResult {
|
||||
gerbilsImported: number
|
||||
littersImported: number
|
||||
contactsImported: number
|
||||
healthRecordsImported: number
|
||||
weightRecordsImported: number
|
||||
photosImported: number
|
||||
message: string
|
||||
}
|
||||
|
||||
async function postUpload<T>(path: string, backup: File, images: File | null): Promise<T> {
|
||||
const form = new FormData()
|
||||
form.append('backup', backup)
|
||||
if (images) form.append('images', images)
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(`${API_BASE_URL}${path}`, { method: 'POST', body: form })
|
||||
} catch {
|
||||
throw new ApiError(de.api.errors.network, null, path)
|
||||
}
|
||||
if (!response.ok) {
|
||||
// 400 trägt eine deutsche Klartext-Fehlermeldung (z. B. „keine _rpro3.db gefunden").
|
||||
let body: unknown
|
||||
let text: string | null = null
|
||||
try {
|
||||
text = await response.clone().text()
|
||||
body = text ? JSON.parse(text) : undefined
|
||||
} catch {
|
||||
body = undefined
|
||||
}
|
||||
const message =
|
||||
response.status === 400 && text
|
||||
? text.replace(/^"|"$/g, '')
|
||||
: response.status >= 500
|
||||
? de.api.errors.server
|
||||
: de.api.errors.unknown
|
||||
throw new ApiError(message, response.status, path, body)
|
||||
}
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
export function analyzeRpro3(backup: File, images: File | null): Promise<Rpro3AnalyzeResult> {
|
||||
return postUpload<Rpro3AnalyzeResult>('/import/rpro3/analyze', backup, images)
|
||||
}
|
||||
|
||||
export function executeRpro3(backup: File, images: File | null): Promise<Rpro3ExecuteResult> {
|
||||
return postUpload<Rpro3ExecuteResult>('/import/rpro3/execute', backup, images)
|
||||
}
|
||||
Reference in New Issue
Block a user