2026-07-26 02:35:44 +02:00
|
|
|
import { apiClient } from './client';
|
|
|
|
|
|
|
|
|
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-08-13 22:43:33 +02:00
|
|
|
export interface ImportError {
|
|
|
|
|
row: number;
|
|
|
|
|
field: string;
|
|
|
|
|
message: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ErrorReport {
|
|
|
|
|
total_errors: number;
|
|
|
|
|
errors: ImportError[];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 02:35:44 +02:00
|
|
|
export interface ImportResult {
|
|
|
|
|
total?: number;
|
2026-08-13 22:43:33 +02:00
|
|
|
succeeded?: number;
|
|
|
|
|
failed?: number;
|
|
|
|
|
status?: 'success' | 'partial_success' | 'failed' | 'pending' | 'processing' | 'completed';
|
|
|
|
|
dry_run?: boolean;
|
|
|
|
|
error_report?: ErrorReport;
|
|
|
|
|
created?: Record<string, unknown>[];
|
2026-07-26 02:35:44 +02:00
|
|
|
errors?: string[];
|
|
|
|
|
warnings?: string[];
|
|
|
|
|
rows?: Record<string, unknown>[];
|
|
|
|
|
preview?: boolean;
|
2026-08-13 22:43:33 +02:00
|
|
|
// 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;
|
2026-07-26 02:35:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type EntityType = 'companies' | 'contacts';
|
2026-08-13 22:43:33 +02:00
|
|
|
export type ExportFormat = 'csv' | 'xlsx' | 'json';
|
2026-07-26 02:35:44 +02:00
|
|
|
|
|
|
|
|
// ─── Import ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-13 22:43:33 +02:00
|
|
|
* Import a CSV/JSON/XLSX file. When dryRun is true, a preview is returned without
|
2026-07-26 02:35:44 +02:00
|
|
|
* writing to the database.
|
|
|
|
|
*/
|
|
|
|
|
export async function importCsv(
|
|
|
|
|
file: File,
|
|
|
|
|
entityType: string,
|
2026-08-13 22:43:33 +02:00
|
|
|
dryRun: boolean,
|
|
|
|
|
fieldMapping?: Record<string, string>,
|
2026-07-26 02:35:44 +02:00
|
|
|
): Promise<ImportResult> {
|
|
|
|
|
const url = dryRun ? '/import/preview' : '/import';
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
formData.append('entity_type', entityType);
|
2026-08-13 22:43:33 +02:00
|
|
|
if (fieldMapping) {
|
|
|
|
|
formData.append('field_mapping', JSON.stringify(fieldMapping));
|
|
|
|
|
}
|
2026-07-26 02:35:44 +02:00
|
|
|
|
|
|
|
|
const response = await apiClient.post<ImportResult>(url, formData, {
|
|
|
|
|
headers: { 'Content-Type': 'multipart/form-data' },
|
|
|
|
|
});
|
|
|
|
|
return response.data;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 22:43:33 +02:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 02:35:44 +02:00
|
|
|
// ─── Export ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-13 22:43:33 +02:00
|
|
|
* Export data as CSV, XLSX, or JSON and trigger a browser download.
|
2026-07-26 02:35:44 +02:00
|
|
|
*/
|
|
|
|
|
export async function exportData(
|
|
|
|
|
entityType: string,
|
2026-08-13 22:43:33 +02:00
|
|
|
format: string,
|
2026-07-26 02:35:44 +02:00
|
|
|
): 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'
|
2026-08-13 22:43:33 +02:00
|
|
|
: format === 'json'
|
|
|
|
|
? 'application/json'
|
|
|
|
|
: 'text/csv',
|
2026-07-26 02:35:44 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|