// ImportDialog — three-step XLSX/CSV/JSON import flow (upload → validate → // import with per-row error report). The template, the accepted columns and // the validation rules all come from the model's ImportSpec served by the // kernel, so this dialog carries no per-model knowledge. Axios-like client is // provided by . import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@asteby/metacore-ui/primitives' import { Progress } from './_primitives' import { toast } from 'sonner' import { FileDown, Loader2, Check, AlertCircle } from 'lucide-react' import type { TableMetadata } from '../types' import { useApi } from '../api-context' import { FilePickButton } from '../file-pick-button' interface ImportDialogProps { open: boolean onOpenChange: (open: boolean) => void model: string metadata: TableMetadata onImported?: () => void } // RowError is one problem the backend reports against one spreadsheet row. // The kernel returns `{row, column, message}` on validate and // `{row, column, error}` on import; older backends returned `{row, field, // message}`. Normalising here keeps the dialog working against every one of // them instead of rendering blank cells. interface RowError { row: number column?: string field?: string message?: string error?: string } function errorColumn(e: RowError): string { return e.column ?? e.field ?? '' } function errorMessage(e: RowError): string { return e.message ?? e.error ?? '' } interface ValidationResult { valid: number skipped: number errors: RowError[] } interface ImportResult { created: number skipped: number errors: RowError[] } type Step = 'upload' | 'validation' | 'results' export function ImportDialog({ open, onOpenChange, model, metadata, onImported, }: ImportDialogProps) { const api = useApi() const { t } = useTranslation() const [step, setStep] = useState('upload') const [file, setFile] = useState(null) const [validating, setValidating] = useState(false) const [importing, setImporting] = useState(false) const [validationResult, setValidationResult] = useState(null) const [importResult, setImportResult] = useState(null) const [progress, setProgress] = useState(0) useEffect(() => { if (open) { setStep('upload') setFile(null) setValidating(false) setImporting(false) setValidationResult(null) setImportResult(null) setProgress(0) } }, [open]) const handleDownloadTemplate = async () => { try { const response = await api.get(`/dynamic/${model}/export/template`, { responseType: 'blob', }) const url = window.URL.createObjectURL(response.data) const link = document.createElement('a') link.href = url link.download = `${model}-plantilla.xlsx` document.body.appendChild(link) link.click() document.body.removeChild(link) window.URL.revokeObjectURL(url) } catch { toast.error('Error al descargar la plantilla') } } const handleValidate = async () => { if (!file) { toast.error('Selecciona un archivo para validar') return } setValidating(true) try { const formData = new FormData() formData.append('file', file) const res = await api.post(`/dynamic/${model}/import/validate`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, }) const data = res.data?.data ?? res.data setValidationResult({ valid: data.rowCount ?? data.valid ?? 0, skipped: data.skipped ?? 0, errors: data.errors ?? [], }) setStep('validation') } catch (err: any) { // Backends may answer a validation failure with a non-2xx status // (e.g. 422) while still carrying the real per-row report in the // body. Render it instead of a body-less "something went wrong" // toast whenever that shape is present. const body = err?.response?.data const payload = body?.data ?? body if (payload && (Array.isArray(payload.errors) || typeof payload.rowCount === 'number' || typeof payload.valid === 'number')) { setValidationResult({ valid: payload.rowCount ?? payload.valid ?? 0, skipped: payload.skipped ?? 0, errors: payload.errors ?? [], }) setStep('validation') } else { toast.error(body?.message || 'Error al validar el archivo') } } finally { setValidating(false) } } const handleImport = async () => { if (!file) return setImporting(true) setProgress(0) try { const formData = new FormData() formData.append('file', file) const res = await api.post(`/dynamic/${model}/import`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent: { loaded: number; total?: number }) => { if (progressEvent.total) { setProgress( Math.round((progressEvent.loaded / progressEvent.total) * 100) ) } }, }) const data = res.data?.data ?? res.data setImportResult({ created: data.created ?? 0, skipped: data.skipped ?? 0, errors: data.failures ?? data.errors ?? [], }) setStep('results') if ((data.created ?? 0) > 0) { onImported?.() } } catch (err: any) { // A partial or total import failure is still a real, displayable // result — the backend computes per-row reasons in `data.failures` // even when it answers with a non-2xx status (e.g. 422 when zero // rows were created). Show that report instead of discarding it // behind a generic toast. const body = err?.response?.data const payload = body?.data ?? body if (payload && (Array.isArray(payload.failures) || Array.isArray(payload.errors) || typeof payload.created === 'number')) { setImportResult({ created: payload.created ?? 0, skipped: payload.skipped ?? 0, errors: payload.failures ?? payload.errors ?? [], }) setStep('results') if ((payload.created ?? 0) > 0) { onImported?.() } } else { toast.error(body?.message || 'Error al importar los datos') } } finally { setImporting(false) setProgress(0) } } const handleClose = () => { onOpenChange(false) } const stepTitle = { upload: 'Subir archivo', validation: 'Validacion', results: 'Resultados', } return ( Importar {metadata.title} {stepTitle[step]}
{step === 'upload' && (

La plantilla trae las columnas, un ejemplo y las instrucciones. Borra la fila de ejemplo antes de subir.

Formatos aceptados: Excel, CSV, JSON

)} {step === 'validation' && validationResult && (
{validationResult.valid} valido(s)
{validationResult.errors.length > 0 && (
{validationResult.errors.length} error(es)
)}
{validationResult.skipped > 0 && (

Se ignoraron {validationResult.skipped} fila(s) de ejemplo de la plantilla.

)} {validationResult.errors.length > 0 && (
Fila Campo Error {validationResult.errors.map((error, idx) => ( {error.row} {errorColumn(error)} {errorMessage(error)} ))}
)} {importing && (

Importando... {progress > 0 ? `${progress}%` : ''}

)}
)} {step === 'results' && importResult && (
{importResult.created > 0 && (
{importResult.created} creado(s)
)} {importResult.errors.length > 0 && (
{importResult.errors.length} error(es)
)}
{importResult.created > 0 && importResult.errors.length === 0 && (
Todos los registros fueron importados correctamente.
)} {importResult.errors.length > 0 && (
Fila Campo Error {importResult.errors.map((error, idx) => ( {error.row} {errorColumn(error)} {errorMessage(error)} ))}
)}
)}
{step === 'upload' && ( <> )} {step === 'validation' && ( <> )} {step === 'results' && ( )}
) }