e7ae0ad5ce
C5-BASE: app/services/import_export_helpers.py (NEU, 352 Zeilen)
- parse_csv/json/xlsx, write_csv/json/xlsx, map_fields, suggest_mapping
- validate_row, build_error_report, build_import_result, detect_format
C5-PREVIEW: POST /import/preview + POST /import/validate
- Preview gibt erste 10 Zeilen + Spalten + Mapping-Vorschlag
- Validate gibt Fehler-Report ohne Import
C5-JOB: app/services/import_export_jobs.py (NEU, 165 Zeilen)
- ARQ Background Job für Files >1000 Zeilen
- Job-Status: pending/processing/completed/partial_success/failed
- GET /import/status/{job_id} — Status + Progress + Fehler-Report
- Partial-Failure: try/except pro Zeile, fehlerhafte gesammelt, erfolgreiche committet
C5-CONTACT+C5-COMPANY: Handler auf Shared Helpers umgestellt
C5-UI: ImportWizard.tsx (5 Steps: Upload→Preview→Validation→Review→Result)
C5-TEST: 45 Tests in test_import_export.py — alle grün
C5-DOC: Plugin-Dev-Guide Kapitel 31 (Import/Export Handler)
206 lines
5.8 KiB
TypeScript
206 lines
5.8 KiB
TypeScript
import { apiClient } from './client';
|
|
|
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
export interface ImportError {
|
|
row: number;
|
|
field: string;
|
|
message: string;
|
|
}
|
|
|
|
export interface ErrorReport {
|
|
total_errors: number;
|
|
errors: ImportError[];
|
|
}
|
|
|
|
export interface ImportResult {
|
|
total?: number;
|
|
succeeded?: number;
|
|
failed?: number;
|
|
status?: 'success' | 'partial_success' | 'failed' | 'pending' | 'processing' | 'completed';
|
|
dry_run?: boolean;
|
|
error_report?: ErrorReport;
|
|
created?: Record<string, unknown>[];
|
|
errors?: string[];
|
|
warnings?: string[];
|
|
rows?: Record<string, unknown>[];
|
|
preview?: boolean;
|
|
// Legacy compat
|
|
valid?: number;
|
|
invalid?: number;
|
|
job_id?: string;
|
|
message?: string;
|
|
}
|
|
|
|
export interface PreviewResult {
|
|
total_rows: number;
|
|
columns: string[];
|
|
preview_rows: Record<string, string>[];
|
|
mapping_suggestion: Record<string, string>;
|
|
target_fields: string[];
|
|
}
|
|
|
|
export interface ValidateResult {
|
|
total: number;
|
|
succeeded: number;
|
|
failed: number;
|
|
status: string;
|
|
dry_run: boolean;
|
|
error_report: ErrorReport;
|
|
created: Record<string, unknown>[];
|
|
}
|
|
|
|
export interface JobStatus {
|
|
status: 'pending' | 'processing' | 'completed' | 'partial_success' | 'failed';
|
|
progress?: number;
|
|
total?: number;
|
|
succeeded?: number;
|
|
failed?: number;
|
|
error_report?: ErrorReport;
|
|
created_count?: number;
|
|
error?: string;
|
|
}
|
|
|
|
export type EntityType = 'companies' | 'contacts';
|
|
export type ExportFormat = 'csv' | 'xlsx' | 'json';
|
|
|
|
// ─── Import ─────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Import a CSV/JSON/XLSX file. When dryRun is true, a preview is returned without
|
|
* writing to the database.
|
|
*/
|
|
export async function importCsv(
|
|
file: File,
|
|
entityType: string,
|
|
dryRun: boolean,
|
|
fieldMapping?: Record<string, string>,
|
|
): Promise<ImportResult> {
|
|
const url = dryRun ? '/import/preview' : '/import';
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('entity_type', entityType);
|
|
if (fieldMapping) {
|
|
formData.append('field_mapping', JSON.stringify(fieldMapping));
|
|
}
|
|
|
|
const response = await apiClient.post<ImportResult>(url, formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Preview a file: parse, return first 10 rows + columns + mapping suggestion.
|
|
*/
|
|
export async function previewImport(
|
|
file: File,
|
|
entityType: string,
|
|
): Promise<PreviewResult> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('entity_type', entityType);
|
|
|
|
const response = await apiClient.post<PreviewResult>('/import/preview', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Validate a file against mapping: returns error report without importing.
|
|
*/
|
|
export async function validateImport(
|
|
file: File,
|
|
entityType: string,
|
|
fieldMapping?: Record<string, string>,
|
|
): Promise<ValidateResult> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('entity_type', entityType);
|
|
if (fieldMapping) {
|
|
formData.append('field_mapping', JSON.stringify(fieldMapping));
|
|
}
|
|
|
|
const response = await apiClient.post<ValidateResult>('/import/validate', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Import contacts specifically.
|
|
*/
|
|
export async function importContacts(
|
|
file: File,
|
|
fieldMapping?: Record<string, string>,
|
|
): Promise<ImportResult> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
if (fieldMapping) {
|
|
formData.append('field_mapping', JSON.stringify(fieldMapping));
|
|
}
|
|
|
|
const response = await apiClient.post<ImportResult>('/import/contacts', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Import companies specifically.
|
|
*/
|
|
export async function importCompanies(
|
|
file: File,
|
|
fieldMapping?: Record<string, string>,
|
|
): Promise<ImportResult> {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
if (fieldMapping) {
|
|
formData.append('field_mapping', JSON.stringify(fieldMapping));
|
|
}
|
|
|
|
const response = await apiClient.post<ImportResult>('/import/companies', formData, {
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
});
|
|
return response.data;
|
|
}
|
|
|
|
/**
|
|
* Get status of a background import job.
|
|
*/
|
|
export async function getImportJobStatus(jobId: string): Promise<JobStatus> {
|
|
const response = await apiClient.get<JobStatus>(`/import/status/${jobId}`);
|
|
return response.data;
|
|
}
|
|
|
|
// ─── Export ─────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Export data as CSV, XLSX, or JSON and trigger a browser download.
|
|
*/
|
|
export async function exportData(
|
|
entityType: string,
|
|
format: string,
|
|
): Promise<void> {
|
|
const url = `/export?entity_type=${encodeURIComponent(entityType)}&format=${encodeURIComponent(format)}`;
|
|
const response = await apiClient.get(url, { responseType: 'blob' });
|
|
|
|
const blob = new Blob([response.data], {
|
|
type: format === 'xlsx'
|
|
? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
: format === 'json'
|
|
? 'application/json'
|
|
: 'text/csv',
|
|
});
|
|
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = blobUrl;
|
|
link.download = `${entityType}_export.${format}`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(blobUrl);
|
|
}
|