252 lines
6.8 KiB
TypeScript
252 lines
6.8 KiB
TypeScript
/**
|
|
* Global Table Import/Export Utility
|
|
* Handles CSV/JSON export and import for any table-based page
|
|
*
|
|
* Usage:
|
|
* ```typescript
|
|
* import { useTableImportExport } from '../../utils/table-import-export';
|
|
*
|
|
* const { handleExport, handleImport } = useTableImportExport({
|
|
* endpoint: '/v1/planner/keywords/export/',
|
|
* filename: 'keywords',
|
|
* columns: columns, // Column config from TablePageTemplate
|
|
* filters: filterValues, // Current filter values
|
|
* });
|
|
* ```
|
|
*/
|
|
|
|
import { API_BASE_URL } from '../services/api';
|
|
|
|
export interface ExportConfig {
|
|
endpoint: string;
|
|
filename: string;
|
|
format?: 'csv' | 'json';
|
|
columns?: Array<{ key: string; label: string }>;
|
|
filters?: Record<string, any>;
|
|
}
|
|
|
|
export interface ImportConfig {
|
|
endpoint: string;
|
|
acceptedFormats?: string[];
|
|
maxFileSize?: number;
|
|
queryParams?: Record<string, any>; // Additional query params (e.g., site_id, sector_id)
|
|
onSuccess?: (result: any) => void;
|
|
onError?: (error: Error) => void;
|
|
}
|
|
|
|
/**
|
|
* Build export URL with filters as query parameters
|
|
*/
|
|
export const buildExportUrl = (config: ExportConfig): string => {
|
|
const params = new URLSearchParams();
|
|
|
|
// Add filters as query params
|
|
if (config.filters) {
|
|
Object.entries(config.filters).forEach(([key, value]) => {
|
|
if (value !== '' && value !== null && value !== undefined) {
|
|
params.append(key, String(value));
|
|
}
|
|
});
|
|
}
|
|
|
|
const queryString = params.toString();
|
|
const endpoint = config.endpoint.endsWith('/')
|
|
? config.endpoint
|
|
: `${config.endpoint}/`;
|
|
|
|
const fullUrl = `${API_BASE_URL}${endpoint}${queryString ? `?${queryString}` : ''}`;
|
|
|
|
|
|
return fullUrl;
|
|
};
|
|
|
|
/**
|
|
* Export table data to CSV or JSON
|
|
*/
|
|
export const exportTableData = async (
|
|
config: ExportConfig,
|
|
onProgress?: (progress: string) => void,
|
|
onError?: (error: Error) => void
|
|
): Promise<void> => {
|
|
const format = config.format || 'csv';
|
|
const url = buildExportUrl(config);
|
|
|
|
|
|
onProgress?.(`Exporting ${format.toUpperCase()}...`);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Export failed: ${response.statusText} - ${errorText}`);
|
|
}
|
|
|
|
const blob = await response.blob();
|
|
const downloadUrl = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = downloadUrl;
|
|
link.download = `${config.filename}.${format}`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(downloadUrl);
|
|
|
|
onProgress?.(`Export successful: ${config.filename}.${format}`);
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Export failed');
|
|
onError?.(err);
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Import table data from file
|
|
*/
|
|
export const importTableData = async (
|
|
file: File,
|
|
config: ImportConfig,
|
|
onProgress?: (progress: string) => void,
|
|
onError?: (error: Error) => void
|
|
): Promise<any> => {
|
|
// Validate file format
|
|
const acceptedFormats = config.acceptedFormats || ['.csv'];
|
|
const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
|
|
|
|
if (!acceptedFormats.includes(fileExtension)) {
|
|
const error = new Error(
|
|
`Invalid file format. Accepted formats: ${acceptedFormats.join(', ')}`
|
|
);
|
|
onError?.(error);
|
|
throw error;
|
|
}
|
|
|
|
// Validate file size
|
|
const maxSize = config.maxFileSize || 5 * 1024 * 1024; // 5MB default
|
|
if (file.size > maxSize) {
|
|
const error = new Error(`File size exceeds ${maxSize / 1024 / 1024}MB limit`);
|
|
onError?.(error);
|
|
throw error;
|
|
}
|
|
|
|
onProgress?.(`Importing ${file.name}...`);
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
try {
|
|
const endpoint = config.endpoint.endsWith('/')
|
|
? config.endpoint
|
|
: `${config.endpoint}/`;
|
|
|
|
// Build query string from queryParams
|
|
const params = new URLSearchParams();
|
|
if (config.queryParams) {
|
|
Object.entries(config.queryParams).forEach(([key, value]) => {
|
|
if (value !== '' && value !== null && value !== undefined) {
|
|
params.append(key, String(value));
|
|
}
|
|
});
|
|
}
|
|
|
|
const queryString = params.toString();
|
|
const url = `${API_BASE_URL}${endpoint}${queryString ? `?${queryString}` : ''}`;
|
|
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'include',
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
throw new Error(`Import failed: ${response.statusText} - ${errorText}`);
|
|
}
|
|
|
|
const result = await response.json();
|
|
onProgress?.(`Import successful: ${result.imported || 0} rows imported`);
|
|
config.onSuccess?.(result);
|
|
return result;
|
|
} catch (error) {
|
|
const err = error instanceof Error ? error : new Error('Import failed');
|
|
onError?.(err);
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* React hook for table import/export functionality
|
|
*/
|
|
export const useTableImportExport = (exportConfig: ExportConfig) => {
|
|
const handleExport = async (
|
|
format: 'csv' | 'json' = 'csv',
|
|
onProgress?: (progress: string) => void,
|
|
onError?: (error: Error) => void
|
|
) => {
|
|
await exportTableData(
|
|
{ ...exportConfig, format },
|
|
onProgress,
|
|
onError
|
|
);
|
|
};
|
|
|
|
const handleImport = async (
|
|
file: File,
|
|
importConfig: ImportConfig,
|
|
onProgress?: (progress: string) => void,
|
|
onError?: (error: Error) => void
|
|
) => {
|
|
return await importTableData(file, importConfig, onProgress, onError);
|
|
};
|
|
|
|
return {
|
|
handleExport,
|
|
handleImport,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Generate CSV from table data (client-side fallback)
|
|
* Used when backend doesn't provide export endpoint
|
|
*/
|
|
export const generateCSVFromTable = (
|
|
data: any[],
|
|
columns: Array<{ key: string; label: string }>,
|
|
filename: string = 'export'
|
|
): void => {
|
|
// Create header row
|
|
const headers = columns.map((col) => col.label).join(',');
|
|
|
|
// Create data rows
|
|
const rows = data.map((row) =>
|
|
columns
|
|
.map((col) => {
|
|
const value = row[col.key];
|
|
// Escape commas and quotes in CSV
|
|
const stringValue = value?.toString() || '';
|
|
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
|
|
return `"${stringValue.replace(/"/g, '""')}"`;
|
|
}
|
|
return stringValue;
|
|
})
|
|
.join(',')
|
|
);
|
|
|
|
const csvContent = [headers, ...rows].join('\n');
|
|
|
|
// Create download
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const downloadUrl = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = downloadUrl;
|
|
link.download = `${filename}.csv`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(downloadUrl);
|
|
};
|
|
|