375 lines
13 KiB
Svelte
375 lines
13 KiB
Svelte
<script lang="ts">
|
|
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
|
import UploadLauncherGrid from '$lib/components/dashboard/csv-upload/UploadLauncherGrid.svelte';
|
|
import ConfigFooter from '$lib/components/dashboard/csv-upload/ConfigFooter.svelte';
|
|
import ProcessingResultModal from '$lib/components/dashboard/csv-upload/ProcessingResultModal.svelte';
|
|
import {
|
|
catalogosConfig,
|
|
transportesConfig,
|
|
importacionConfig,
|
|
exportacionConfig,
|
|
tabSettings,
|
|
type CsvUploadItem
|
|
} from '$lib/config/csv-upload';
|
|
import { api } from '$lib/api';
|
|
import { toast } from 'svelte-sonner';
|
|
import { companyStore } from '$lib/stores/company.svelte';
|
|
|
|
// We no longer need modal state
|
|
let activeTab = $state('catalogos');
|
|
|
|
let isUploading = $state(false);
|
|
let currentJobId = $state<string | null>(null);
|
|
let activeModelTarget = $state<string | null>(null);
|
|
let scanResults = $state<any>(null);
|
|
let commitResults = $state<any>(null);
|
|
let showResultModal = $state(false);
|
|
// Cuando es true, usamos API de importación de Agentes Aduanales (customs_brokers/imports)
|
|
let useCustomsBrokerImport = $state(false);
|
|
// Cuando es true, usamos API de importación de Clientes y Proveedores (clients_and_providers/imports)
|
|
let useClientProviderImport = $state(false);
|
|
// Cuando es true, usamos API de importación de Tipos de Cambio (exchange_rate/imports)
|
|
let useExchangeRateImport = $state(false);
|
|
// Cuando es true, usamos API de importación de Fracción Americana (us_tariff_fractions/imports)
|
|
let useAmericanFractionImport = $state(false);
|
|
// Cuando es true, usamos API de importación de Pedimentos (pedimentos/imports)
|
|
let usePedimentosImport = $state(false);
|
|
|
|
// Initialize settings for all tabs upfront to avoid reactivity loops
|
|
let allSettings = $state<Record<string, any>>(() => {
|
|
const initial: Record<string, any> = {};
|
|
for (const tab in tabSettings) {
|
|
initial[tab] = {};
|
|
tabSettings[tab].forEach((f) => {
|
|
initial[tab][f.name] = f.defaultValue;
|
|
});
|
|
}
|
|
return initial;
|
|
});
|
|
|
|
async function handleUpload(file: File, config: CsvUploadItem) {
|
|
console.log('handleUpload started', { file, config });
|
|
isUploading = true;
|
|
activeModelTarget = config.modelTarget || null;
|
|
scanResults = null;
|
|
useCustomsBrokerImport = config.id === 'customs_brokers';
|
|
useClientProviderImport = config.id === 'clients_providers';
|
|
useExchangeRateImport = config.id === 'exchange_rates';
|
|
useAmericanFractionImport = config.id === 'american_fractions';
|
|
usePedimentosImport = config.id === 'pedimentos';
|
|
|
|
const companyId = companyStore.activeCompany?.id || 1;
|
|
|
|
if (useCustomsBrokerImport) {
|
|
try {
|
|
const res = await api.customsBrokerImports.upload(file, companyId);
|
|
if (res.data?.job_id) {
|
|
currentJobId = res.data.job_id;
|
|
pollStatus();
|
|
} else {
|
|
toast.error(res.error || 'Error al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
} catch (e) {
|
|
console.error('Upload exception', e);
|
|
toast.error('Error inesperado al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (useClientProviderImport) {
|
|
try {
|
|
const res = await api.clientProviderImports.upload(file, companyId);
|
|
if (res.data?.job_id) {
|
|
currentJobId = res.data.job_id;
|
|
pollStatus();
|
|
} else {
|
|
toast.error(res.error || 'Error al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
} catch (e) {
|
|
console.error('Upload exception', e);
|
|
toast.error('Error inesperado al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (useExchangeRateImport) {
|
|
try {
|
|
const res = await api.exchangeRateImports.upload(file, companyId);
|
|
if (res.data?.job_id) {
|
|
currentJobId = res.data.job_id;
|
|
pollStatus();
|
|
} else {
|
|
toast.error(res.error || 'Error al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
} catch (e) {
|
|
console.error('Upload exception', e);
|
|
toast.error('Error inesperado al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (useAmericanFractionImport) {
|
|
try {
|
|
const res = await api.americanFractionImports.upload(file, companyId);
|
|
if (res.data?.job_id) {
|
|
currentJobId = res.data.job_id;
|
|
pollStatus();
|
|
} else {
|
|
toast.error(res.error || 'Error al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
} catch (e) {
|
|
console.error('Upload exception', e);
|
|
toast.error('Error inesperado al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (usePedimentosImport) {
|
|
try {
|
|
const res = await api.pedimentosImports.upload(file, companyId);
|
|
if (res.data?.job_id) {
|
|
currentJobId = res.data.job_id;
|
|
pollStatus();
|
|
} else {
|
|
toast.error(res.error || 'Error al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
} catch (e) {
|
|
console.error('Upload exception', e);
|
|
toast.error('Error inesperado al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const currentSettings = allSettings[activeTab] || {};
|
|
const footerConfig = { ...currentSettings };
|
|
if (activeTab === 'importacion') {
|
|
footerConfig.invoice_type = config.id?.startsWith('imp_def_') ? 'DEF' : 'TEM';
|
|
}
|
|
const opType = activeTab === 'exportacion' ? 'exp' : 'imp';
|
|
|
|
try {
|
|
const res = await api.imports.upload(
|
|
file,
|
|
config.modelTarget || '',
|
|
footerConfig,
|
|
companyId,
|
|
opType,
|
|
config.id
|
|
);
|
|
if (res.data?.job_id) {
|
|
currentJobId = res.data.job_id;
|
|
pollStatus();
|
|
} else {
|
|
toast.error(res.error || 'Error al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
} catch (e) {
|
|
console.error('Upload exception', e);
|
|
toast.error('Error inesperado al subir el archivo');
|
|
isUploading = false;
|
|
}
|
|
}
|
|
|
|
async function pollStatus() {
|
|
if (!currentJobId) return;
|
|
|
|
try {
|
|
const res = useCustomsBrokerImport
|
|
? await api.customsBrokerImports.status(currentJobId)
|
|
: useClientProviderImport
|
|
? await api.clientProviderImports.status(currentJobId)
|
|
: useExchangeRateImport
|
|
? await api.exchangeRateImports.status(currentJobId)
|
|
: useAmericanFractionImport
|
|
? await api.americanFractionImports.status(currentJobId)
|
|
: usePedimentosImport
|
|
? await api.pedimentosImports.status(currentJobId)
|
|
: await api.imports.status(currentJobId);
|
|
console.log('Poll response', res);
|
|
if (res.error && !res.data) {
|
|
toast.error(res.error || 'Error al consultar el estado');
|
|
isUploading = false;
|
|
currentJobId = null;
|
|
return;
|
|
}
|
|
if (res.data?.status === 'waiting_confirmation') {
|
|
scanResults = res.data;
|
|
showResultModal = true;
|
|
toast.success('Escaneo completado. Revisa los resultados.');
|
|
isUploading = false;
|
|
} else if (res.data?.status === 'failed' || res.data?.status === 'FAILURE') {
|
|
const errRaw = res.data.error;
|
|
const errText =
|
|
typeof errRaw === 'string'
|
|
? errRaw.includes('finished') && errRaw.includes('inserted')
|
|
? 'La importación pudo completarse. Revisa el listado de registros.'
|
|
: errRaw
|
|
: (errRaw?.message ?? 'Error desconocido');
|
|
toast.error('Error en el procesamiento: ' + errText);
|
|
isUploading = false;
|
|
currentJobId = null;
|
|
scanResults = null;
|
|
commitResults = null;
|
|
showResultModal = false;
|
|
} else if (res.data?.status === 'warning') {
|
|
// Caso cuando no se insertaron registros pero hay información de rechazo
|
|
commitResults = res.data;
|
|
showResultModal = true;
|
|
const inserted = res.data?.inserted || 0;
|
|
const skippedInvalid = res.data?.skipped_invalid || 0;
|
|
const skippedFk = res.data?.skipped_missing_fk || 0;
|
|
const skippedDup = res.data?.skipped_duplicate || 0;
|
|
const totalSkipped = skippedInvalid + skippedFk + skippedDup;
|
|
|
|
if (inserted === 0) {
|
|
toast.error(`No se insertaron registros. ${totalSkipped} fueron rechazados.`);
|
|
} else {
|
|
toast.warning(`Solo se insertaron ${inserted} de ${inserted + totalSkipped} registros.`);
|
|
}
|
|
isUploading = false;
|
|
} else if (res.data?.status === 'finished') {
|
|
commitResults = res.data;
|
|
showResultModal = true;
|
|
const inserted = res.data?.inserted || 0;
|
|
const skippedInvalid = res.data?.skipped_invalid || 0;
|
|
const skippedFk = res.data?.skipped_missing_fk || 0;
|
|
const skippedDup = res.data?.skipped_duplicate || 0;
|
|
const skippedDetails = res.data?.skipped_details || [];
|
|
|
|
if (inserted > 0) {
|
|
toast.success(`Importación completada: ${inserted} registros insertados`);
|
|
if (skippedInvalid > 0 || skippedFk > 0 || skippedDup > 0) {
|
|
const totalSkipped = skippedInvalid + skippedFk + skippedDup;
|
|
toast.warning(`${totalSkipped} registros fueron rechazados`);
|
|
}
|
|
} else {
|
|
toast.error('No se insertaron registros. Revisa los errores a continuación.');
|
|
}
|
|
isUploading = false;
|
|
} else {
|
|
// Continue polling
|
|
console.log('Status not final, polling again in 2s...', res.data?.status);
|
|
setTimeout(pollStatus, 2000);
|
|
}
|
|
} catch (e) {
|
|
console.error('Poll exception', e);
|
|
// Retry on network error? Or fail?
|
|
// For now, let's keep retrying a few times or hard fail.
|
|
// Let's just log and retry.
|
|
setTimeout(pollStatus, 2000);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col h-[calc(100vh-4rem)] -m-4 overflow-hidden">
|
|
<!-- Scrollable Content Area -->
|
|
<div class="flex-1 overflow-y-auto p-4 md:p-8 space-y-4">
|
|
<div class="flex items-center gap-4">
|
|
<h1 class="text-lg font-semibold md:text-2xl">Importación Masiva de Datos (CSV)</h1>
|
|
</div>
|
|
|
|
<Tabs.Root bind:value={activeTab} class="w-full">
|
|
<Tabs.List class="grid w-full grid-cols-2 md:grid-cols-4 lg:w-auto">
|
|
<Tabs.Trigger value="catalogos">Catálogos</Tabs.Trigger>
|
|
<Tabs.Trigger value="transportes">Transportes</Tabs.Trigger>
|
|
<Tabs.Trigger value="importacion">Importación</Tabs.Trigger>
|
|
<Tabs.Trigger value="exportacion">Exportación</Tabs.Trigger>
|
|
</Tabs.List>
|
|
|
|
<div class="mt-6">
|
|
<Tabs.Content value="catalogos" class="space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-medium tracking-tight">Catálogos Generales</h2>
|
|
</div>
|
|
<UploadLauncherGrid items={catalogosConfig} onUpload={handleUpload} />
|
|
</Tabs.Content>
|
|
|
|
<Tabs.Content value="transportes" class="space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-medium tracking-tight">Logística y Transporte</h2>
|
|
</div>
|
|
<UploadLauncherGrid items={transportesConfig} onUpload={handleUpload} />
|
|
</Tabs.Content>
|
|
|
|
<Tabs.Content value="importacion" class="space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-medium tracking-tight">Operaciones de Importación</h2>
|
|
</div>
|
|
<UploadLauncherGrid items={importacionConfig} onUpload={handleUpload} />
|
|
</Tabs.Content>
|
|
|
|
<Tabs.Content value="exportacion" class="space-y-4">
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-medium tracking-tight">Operaciones de Exportación</h2>
|
|
</div>
|
|
<UploadLauncherGrid items={exportacionConfig} onUpload={handleUpload} />
|
|
</Tabs.Content>
|
|
</div>
|
|
</Tabs.Root>
|
|
|
|
<div class="h-4"></div>
|
|
</div>
|
|
|
|
<!-- Fixed Footer Area -->
|
|
{#if allSettings[activeTab]}
|
|
<div class="flex-none z-20">
|
|
<ConfigFooter {activeTab} bind:settings={allSettings[activeTab]} />
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if scanResults || commitResults}
|
|
<ProcessingResultModal
|
|
bind:open={showResultModal}
|
|
{scanResults}
|
|
{commitResults}
|
|
{isUploading}
|
|
onConfirm={async () => {
|
|
if (!currentJobId) return;
|
|
try {
|
|
isUploading = true;
|
|
const res = useCustomsBrokerImport
|
|
? await api.customsBrokerImports.commit(currentJobId)
|
|
: useClientProviderImport
|
|
? await api.clientProviderImports.commit(currentJobId)
|
|
: useExchangeRateImport
|
|
? await api.exchangeRateImports.commit(currentJobId)
|
|
: useAmericanFractionImport
|
|
? await api.americanFractionImports.commit(currentJobId)
|
|
: usePedimentosImport
|
|
? await api.pedimentosImports.commit(currentJobId)
|
|
: await api.imports.commit(currentJobId, activeModelTarget || '');
|
|
if (res.data?.commit_job_id) {
|
|
currentJobId = res.data.commit_job_id;
|
|
pollStatus();
|
|
}
|
|
} catch (err) {
|
|
toast.error('Error al iniciar la importación');
|
|
isUploading = false;
|
|
}
|
|
}}
|
|
onCancel={() => {
|
|
currentJobId = null;
|
|
scanResults = null;
|
|
commitResults = null;
|
|
showResultModal = false;
|
|
}}
|
|
onClose={() => {
|
|
currentJobId = null;
|
|
scanResults = null;
|
|
commitResults = null;
|
|
showResultModal = false;
|
|
}}
|
|
/>
|
|
{/if}
|