fix/carga-csv-ergonomia
This commit is contained in:
@@ -681,16 +681,23 @@ export const api = {
|
||||
* Returns blob and suggested filename for the browser download.
|
||||
*/
|
||||
async getCsvTemplateDownload(
|
||||
templateId: string
|
||||
templateId: string,
|
||||
locale?: string
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
const response = await fetch(`${API_BASE_URL}/v1/a76/csv-templates/${templateId}`, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
credentials: 'include'
|
||||
});
|
||||
const loc = locale === 'en' ? 'en' : 'es';
|
||||
const qs = new URLSearchParams({ locale: loc });
|
||||
const response = await fetch(
|
||||
`${API_BASE_URL}/v1/a76/csv-templates/${templateId}?${qs.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers,
|
||||
credentials: 'include',
|
||||
cache: 'no-store'
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const msg = response.status === 404 ? 'Plantilla no encontrada' : `Error ${response.status}`;
|
||||
throw new Error(msg);
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group/index.js';
|
||||
import { Settings2 } from 'lucide-svelte';
|
||||
import { tabSettings } from '$lib/config/csv-upload';
|
||||
import { csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
const TAB_LABEL: Record<string, string> = {
|
||||
catalogos: 'tab_catalogos',
|
||||
transportes: 'tab_transportes',
|
||||
importacion: 'tab_importacion',
|
||||
exportacion: 'tab_exportacion'
|
||||
};
|
||||
|
||||
let {
|
||||
activeTab,
|
||||
@@ -38,7 +46,9 @@
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2 text-muted-foreground border-b pb-2">
|
||||
<Settings2 class="h-4 w-4" />
|
||||
<span class="text-xs font-semibold uppercase tracking-wider">Configuración: {activeTab}</span>
|
||||
<span class="text-xs font-semibold uppercase tracking-wider"
|
||||
>{csvMsg('config_prefix')}: {csvMsg(TAB_LABEL[activeTab] ?? activeTab)}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if currentFields.length > 0}
|
||||
@@ -47,7 +57,7 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if field.type !== 'boolean'}
|
||||
<Label class="text-xs font-medium text-muted-foreground uppercase"
|
||||
>{field.label}</Label
|
||||
>{csvMsg(field.labelKey)}</Label
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -59,7 +69,7 @@
|
||||
<Label
|
||||
for={field.name}
|
||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
||||
>{field.label}</Label
|
||||
>{csvMsg(field.labelKey)}</Label
|
||||
>
|
||||
</div>
|
||||
{:else if field.type === 'select' && field.options}
|
||||
@@ -68,7 +78,7 @@
|
||||
bind:value={settings[field.name]}
|
||||
>
|
||||
{#each field.options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
<option value={opt.value}>{csvMsg(opt.labelKey)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if field.type === 'radio' && field.options}
|
||||
@@ -79,7 +89,7 @@
|
||||
{#each field.options as opt}
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value={opt.value} id={`${field.name}-${opt.value}`} />
|
||||
<Label for={`${field.name}-${opt.value}`}>{opt.label}</Label>
|
||||
<Label for={`${field.name}-${opt.value}`}>{csvMsg(opt.labelKey)}</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</RadioGroup.Root>
|
||||
@@ -89,7 +99,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center h-8 text-sm text-muted-foreground italic">
|
||||
No hay configuraciones específicas para este módulo.
|
||||
{csvMsg('config_empty')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
import { Settings2 } from 'lucide-svelte';
|
||||
import { globalCsvParams, tabSettings, type CsvUploadField } from '$lib/config/csv-upload';
|
||||
import { cn } from '$lib/utils';
|
||||
import { csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
const TAB_LABEL: Record<string, string> = {
|
||||
catalogos: 'tab_catalogos',
|
||||
transportes: 'tab_transportes',
|
||||
importacion: 'tab_importacion',
|
||||
exportacion: 'tab_exportacion'
|
||||
};
|
||||
|
||||
let {
|
||||
globalSettings = $bindable(),
|
||||
@@ -45,13 +53,13 @@
|
||||
<div class="flex items-center gap-2 text-muted-foreground border-r border-border pr-4">
|
||||
<Settings2 class="h-4 w-4 shrink-0" />
|
||||
<span class="text-xs font-semibold uppercase tracking-wider whitespace-nowrap"
|
||||
>Parámetros globales</span
|
||||
>{csvMsg('params_header')}</span
|
||||
>
|
||||
</div>
|
||||
{#each globalCsvParams as param}
|
||||
<div class="flex flex-col gap-1">
|
||||
<Label for="global-{param.name}" class="text-xs font-medium text-muted-foreground"
|
||||
>{param.label}</Label
|
||||
>{csvMsg(param.labelKey)}</Label
|
||||
>
|
||||
<select
|
||||
id="global-{param.name}"
|
||||
@@ -62,7 +70,7 @@
|
||||
}}
|
||||
>
|
||||
{#each param.options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
<option value={opt.value}>{csvMsg(opt.labelKey)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
@@ -73,13 +81,13 @@
|
||||
{#if currentTabFields.length > 0}
|
||||
<div class="flex flex-wrap items-center gap-4 border-l border-border pl-4">
|
||||
<span class="text-xs font-semibold uppercase tracking-wider text-muted-foreground whitespace-nowrap"
|
||||
>Configuración: {activeTab}</span
|
||||
>{csvMsg('config_prefix')}: {csvMsg(TAB_LABEL[activeTab] ?? activeTab)}</span
|
||||
>
|
||||
{#each currentTabFields as field}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#if field.type !== 'boolean'}
|
||||
<Label for="tab-{field.name}" class="text-xs font-medium text-muted-foreground"
|
||||
>{field.label}</Label
|
||||
>{csvMsg(field.labelKey)}</Label
|
||||
>
|
||||
{/if}
|
||||
{#if field.type === 'select' && field.options}
|
||||
@@ -92,7 +100,7 @@
|
||||
}}
|
||||
>
|
||||
{#each field.options as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
<option value={opt.value}>{csvMsg(opt.labelKey)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if field.type === 'radio' && field.options}
|
||||
@@ -108,7 +116,7 @@
|
||||
if (tabSettingsValues) tabSettingsValues[field.name] = opt.value;
|
||||
}}
|
||||
/>
|
||||
{opt.label}
|
||||
{csvMsg(opt.labelKey)}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -121,7 +129,7 @@
|
||||
if (tabSettingsValues) tabSettingsValues[field.name] = e.currentTarget.checked;
|
||||
}}
|
||||
/>
|
||||
{field.label}
|
||||
{csvMsg(field.labelKey)}
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
} from '$lib/csv-import-pending';
|
||||
import { fetchCsvImportStatus, isWaitingConfirmationPayload } from '$lib/csv-import-status-api';
|
||||
import { Loader2, RefreshCw } from 'lucide-svelte';
|
||||
import { csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
type ValidatedRow = CsvImportPendingEntry & { checking?: boolean };
|
||||
|
||||
@@ -36,22 +37,9 @@
|
||||
}
|
||||
|
||||
function profileLabel(p: CsvImportPendingEntry['profile']): string {
|
||||
const map: Record<CsvImportPendingEntry['profile'], string> = {
|
||||
customs_brokers: 'Agentes aduanales',
|
||||
clients_providers: 'Clientes / proveedores',
|
||||
exchange_rates: 'Tipos de cambio',
|
||||
pedimentos: 'Pedimentos',
|
||||
material_classes: 'Clases de material',
|
||||
vehicles: 'Vehículos',
|
||||
drivers: 'Conductores',
|
||||
trailers: 'Remolques',
|
||||
transporters: 'Transportistas',
|
||||
part_numbers: 'Números de parte',
|
||||
boms: 'BOMs',
|
||||
exportacion: 'Exportación (operaciones)',
|
||||
imports: 'Importación (operaciones)'
|
||||
};
|
||||
return map[p] ?? p;
|
||||
const key = `pending.profiles.${p}` as const;
|
||||
const t = csvMsg(key);
|
||||
return t === key ? p : t;
|
||||
}
|
||||
|
||||
function isStaleJob(status: number, err: string | undefined): boolean {
|
||||
@@ -134,7 +122,7 @@
|
||||
</script>
|
||||
|
||||
<Button variant="outline" size="sm" class="shrink-0" type="button" onclick={() => (open = true)}>
|
||||
Pendientes
|
||||
{csvMsg('pending.badge')}
|
||||
{#if badgeCount > 0}
|
||||
<span class="ml-1.5 rounded-full bg-primary/15 px-2 py-0.5 text-xs font-semibold text-primary">
|
||||
{badgeCount}
|
||||
@@ -145,10 +133,9 @@
|
||||
<Sheet.Root bind:open>
|
||||
<Sheet.Content side="right" class="flex w-full max-w-lg flex-col sm:max-w-xl">
|
||||
<Sheet.Header>
|
||||
<Sheet.Title>Importaciones pendientes de confirmar</Sheet.Title>
|
||||
<Sheet.Title>{csvMsg('pending.title')}</Sheet.Title>
|
||||
<Sheet.Description>
|
||||
Escaneos listos para insertar en base de datos. Si el trabajo ya expiró en el servidor, desaparecerán al
|
||||
actualizar.
|
||||
{csvMsg('pending.description')}
|
||||
</Sheet.Description>
|
||||
</Sheet.Header>
|
||||
<div class="flex items-center justify-end gap-2 border-b px-4 py-2">
|
||||
@@ -158,14 +145,14 @@
|
||||
{:else}
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
Actualizar
|
||||
{csvMsg('pending.refresh')}
|
||||
</Button>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
||||
{#if rows.length === 0 && !refreshing}
|
||||
<p class="text-sm text-muted-foreground">No hay importaciones pendientes para esta empresa.</p>
|
||||
<p class="text-sm text-muted-foreground">{csvMsg('pending.empty')}</p>
|
||||
{:else if rows.length === 0 && refreshing}
|
||||
<p class="text-sm text-muted-foreground">Comprobando con el servidor…</p>
|
||||
<p class="text-sm text-muted-foreground">{csvMsg('pending.checking')}</p>
|
||||
{:else}
|
||||
<ul class="space-y-3">
|
||||
{#each rows as row (row.jobId)}
|
||||
@@ -177,10 +164,10 @@
|
||||
{#if row.totalRows != null || row.validRows != null}
|
||||
<div class="mt-1 text-xs text-muted-foreground">
|
||||
{#if row.totalRows != null}
|
||||
Total filas: {row.totalRows}
|
||||
{csvMsg('pending.total_rows')}: {row.totalRows}
|
||||
{/if}
|
||||
{#if row.validRows != null}
|
||||
<span class={row.totalRows != null ? ' · ' : ''}>Válidas: {row.validRows}</span>
|
||||
<span class={row.totalRows != null ? ' · ' : ''}>{csvMsg('pending.valid_rows')}: {row.validRows}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -201,9 +188,9 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reanudar
|
||||
{csvMsg('pending.resume')}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onclick={() => removeLocal(row.jobId)}>Quitar</Button>
|
||||
<Button size="sm" variant="ghost" onclick={() => removeLocal(row.jobId)}>{csvMsg('pending.remove')}</Button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
criticalReferenceGaps,
|
||||
referenceStateReady
|
||||
} from '$lib/csv-import-commit-metrics';
|
||||
import { csvFmt, csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -249,16 +250,16 @@
|
||||
<div class="flex-1">
|
||||
<Dialog.Title class="text-xl font-semibold tracking-tight text-foreground">
|
||||
{#if isPending}
|
||||
Validación de Importación
|
||||
{csvMsg('modal.title_pending')}
|
||||
{:else if isFinished}
|
||||
{hasErrors ? 'Importación con Observaciones' : 'Importación Exitosa'}
|
||||
{hasErrors ? csvMsg('modal.title_warning') : csvMsg('modal.title_success')}
|
||||
{/if}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description class="mt-1 text-muted-foreground">
|
||||
{#if isPending}
|
||||
Revise el análisis preliminar antes de confirmar la carga de datos.
|
||||
{csvMsg('modal.desc_pending')}
|
||||
{:else if isFinished}
|
||||
El proceso de importación ha finalizado.
|
||||
{csvMsg('modal.desc_done')}
|
||||
{/if}
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
@@ -274,7 +275,7 @@
|
||||
class="bg-card p-4 rounded-lg border flex flex-col items-center justify-center text-center shadow-sm"
|
||||
>
|
||||
<span class="text-muted-foreground text-xs uppercase font-bold tracking-wider mb-1"
|
||||
>Total Filas</span
|
||||
>{csvMsg('modal.total_rows')}</span
|
||||
>
|
||||
<span class="text-2xl font-bold text-foreground">{scanResults.total_rows || 0}</span>
|
||||
</div>
|
||||
@@ -285,7 +286,7 @@
|
||||
>
|
||||
<span
|
||||
class="text-green-600 dark:text-green-400 text-xs uppercase font-bold tracking-wider mb-1"
|
||||
>Válidos</span
|
||||
>{csvMsg('modal.valid_rows')}</span
|
||||
>
|
||||
<span class="text-2xl font-bold text-green-700 dark:text-green-300"
|
||||
>{scanResults.valid_rows || 0}</span
|
||||
@@ -297,7 +298,7 @@
|
||||
class="bg-destructive/5 p-4 rounded-lg border border-destructive/10 flex flex-col items-center justify-center text-center shadow-sm"
|
||||
>
|
||||
<span class="text-destructive text-xs uppercase font-bold tracking-wider mb-1"
|
||||
>Errores</span
|
||||
>{csvMsg('modal.errors')}</span
|
||||
>
|
||||
<span class="text-2xl font-bold text-destructive">{scanResults.error_count || 0}</span>
|
||||
</div>
|
||||
@@ -327,10 +328,9 @@
|
||||
>
|
||||
<XCircle class="w-5 h-5 text-destructive mt-0.5 shrink-0" />
|
||||
<div class="text-sm text-destructive-foreground/90">
|
||||
<p class="font-semibold mb-1">Se detectaron problemas en el archivo</p>
|
||||
<p class="font-semibold mb-1">{csvMsg('modal.scan_problems_title')}</p>
|
||||
<p>
|
||||
Corrija los datos indicados abajo en su CSV y vuelva a subir, o confirme para
|
||||
importar solo las filas válidas (las erróneas se omitirán).
|
||||
{csvMsg('modal.scan_problems_body')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -338,13 +338,13 @@
|
||||
<div class="border rounded-lg overflow-hidden shadow-sm">
|
||||
<div class="bg-muted/50 px-4 py-2 border-b flex justify-between items-center">
|
||||
<h5 class="text-xs font-bold text-foreground uppercase tracking-wide">
|
||||
Detalle de errores (para corregir en el CSV)
|
||||
{csvMsg('modal.errors_heading')}
|
||||
</h5>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
|
||||
>
|
||||
{scanErrorsShown} de {scanErrorsTotal} error(es)
|
||||
{csvFmt('modal.errors_badge', { shown: scanErrorsShown, total: scanErrorsTotal })}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -352,7 +352,7 @@
|
||||
class="h-7 px-2 text-xs"
|
||||
>
|
||||
<FileText class="w-3.5 h-3.5 mr-1" />
|
||||
Descargar CSV
|
||||
{csvMsg('modal.download_csv')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -362,10 +362,10 @@
|
||||
class="text-muted-foreground font-medium bg-muted/30 sticky top-0 z-10 shadow-sm backdrop-blur-sm"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-4 py-2 w-16">Línea</th>
|
||||
<th class="px-4 py-2 w-40">Columna</th>
|
||||
<th class="px-4 py-2">Mensaje</th>
|
||||
<th class="px-4 py-2 min-w-[240px]">Solución</th>
|
||||
<th class="px-4 py-2 w-16">{csvMsg('modal.th_line')}</th>
|
||||
<th class="px-4 py-2 w-40">{csvMsg('modal.th_column')}</th>
|
||||
<th class="px-4 py-2">{csvMsg('modal.th_message')}</th>
|
||||
<th class="px-4 py-2 min-w-[240px]">{csvMsg('modal.th_solution')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@@ -382,21 +382,21 @@
|
||||
</div>
|
||||
{#if scanErrorsTruncated}
|
||||
<p class="text-xs text-muted-foreground px-4 py-2">
|
||||
Para consultar el resto de errores, descargue el CSV.
|
||||
{csvMsg('modal.errors_truncated')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground mt-1">
|
||||
Se detectaron {scanResults.error_count} fila(s) con errores pero el detalle no está disponible. Asegúrese de que el servidor esté actualizado y vuelva a subir el archivo.
|
||||
{csvFmt('modal.errors_missing_detail', { count: scanResults.error_count })}
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="rounded-md bg-primary/5 border border-primary/10 p-4 flex items-start gap-3">
|
||||
<CheckCircle2 class="w-5 h-5 text-primary mt-0.5 shrink-0" />
|
||||
<div class="text-sm text-primary/90">
|
||||
<p class="font-semibold mb-1">Archivo validado correctamente</p>
|
||||
<p>Todos los registros parecen correctos y listos para importar.</p>
|
||||
<p class="font-semibold mb-1">{csvMsg('modal.scan_ok_title')}</p>
|
||||
<p>{csvMsg('modal.scan_ok_body')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -413,7 +413,7 @@
|
||||
<CheckCircle2 class="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider text-green-600 dark:text-green-400"
|
||||
>Insertados</span
|
||||
>{csvMsg('modal.inserted')}</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-3xl font-bold text-green-700 dark:text-green-300">{insertedCount}</span>
|
||||
@@ -425,7 +425,7 @@
|
||||
<CheckCircle2 class="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
<span
|
||||
class="text-xs font-bold uppercase tracking-wider text-green-600 dark:text-green-400"
|
||||
>Actualizados</span
|
||||
>{csvMsg('modal.updated')}</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-3xl font-bold text-green-700 dark:text-green-300">{updatedCount}</span>
|
||||
@@ -436,13 +436,13 @@
|
||||
<div class="mb-1 flex items-center gap-2">
|
||||
<XCircle class="h-4 w-4 text-destructive" />
|
||||
<span class="text-xs font-bold uppercase tracking-wider text-destructive"
|
||||
>Rechazados</span
|
||||
>{csvMsg('modal.rejected')}</span
|
||||
>
|
||||
</div>
|
||||
<span class="text-3xl font-bold text-destructive">{totalSkipped}</span>
|
||||
{#if totalSkipped > 0 && commitResults.skipped_details && commitResults.skipped_details.length > 0}
|
||||
<p class="mt-2 text-xs text-muted-foreground">
|
||||
Revisa el detalle por línea en la tabla inferior.
|
||||
{csvMsg('modal.rejected_hint')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -456,20 +456,17 @@
|
||||
{#if refGaps > 0}
|
||||
<AlertTriangle class="mt-0.5 h-5 w-5 shrink-0 text-amber-600 dark:text-amber-500" />
|
||||
<div>
|
||||
<p class="font-semibold text-foreground">Brechas de referencia (FK / catálogos)</p>
|
||||
<p class="font-semibold text-foreground">{csvMsg('modal.ref_gaps_title')}</p>
|
||||
<p class="mt-1 text-muted-foreground">
|
||||
Hay {refGaps} brecha(s) crítica(s) de referencia. Revisa catálogos y el detalle de
|
||||
filas rechazadas antes de reintentar.
|
||||
{csvFmt('modal.ref_gaps_body', { n: refGaps })}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<CheckCircle2 class="mt-0.5 h-5 w-5 shrink-0 text-green-600 dark:text-green-500" />
|
||||
<div>
|
||||
<p class="font-semibold text-foreground">Estado de referencias</p>
|
||||
<p class="font-semibold text-foreground">{csvMsg('modal.ref_state_title')}</p>
|
||||
<p class="mt-1 text-muted-foreground">
|
||||
{refReady
|
||||
? 'Referencias listas para operar (sin brechas críticas reportadas).'
|
||||
: 'Sin brechas numéricas; revisa el mensaje del servidor si aplica.'}
|
||||
{refReady ? csvMsg('modal.ref_state_ok') : csvMsg('modal.ref_state_other')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -483,7 +480,7 @@
|
||||
{#if commitSkippedSummary.length > 0}
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
||||
Resumen de motivos de rechazo
|
||||
{csvMsg('modal.skipped_reasons_heading')}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each commitSkippedSummary as item}
|
||||
@@ -504,13 +501,13 @@
|
||||
<div id="detalle-errores-import" class="border rounded-lg overflow-hidden mt-2 shadow-sm">
|
||||
<div class="bg-muted/50 px-4 py-2 border-b flex justify-between items-center">
|
||||
<h5 class="text-xs font-bold text-foreground uppercase tracking-wide">
|
||||
Detalle de Errores
|
||||
{csvMsg('modal.commit_errors_heading')}
|
||||
</h5>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
|
||||
>
|
||||
{commitResults.skipped_details.length} filas
|
||||
{csvFmt('modal.rows_badge', { n: commitResults.skipped_details.length })}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -518,7 +515,7 @@
|
||||
class="h-7 px-2 text-xs"
|
||||
>
|
||||
<FileText class="w-3.5 h-3.5 mr-1" />
|
||||
Descargar CSV
|
||||
{csvMsg('modal.download_csv')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -528,10 +525,10 @@
|
||||
class="text-muted-foreground font-medium bg-muted/30 sticky top-0 z-10 shadow-sm backdrop-blur-sm"
|
||||
>
|
||||
<tr>
|
||||
<th class="px-4 py-2 w-20">Línea</th>
|
||||
<th class="px-4 py-2 w-32">Referencia</th>
|
||||
<th class="px-4 py-2">Motivo</th>
|
||||
<th class="px-4 py-2 min-w-[240px]">Solución</th>
|
||||
<th class="px-4 py-2 w-20">{csvMsg('modal.th_line')}</th>
|
||||
<th class="px-4 py-2 w-32">{csvMsg('modal.th_reference')}</th>
|
||||
<th class="px-4 py-2">{csvMsg('modal.th_reason')}</th>
|
||||
<th class="px-4 py-2 min-w-[240px]">{csvMsg('modal.th_solution')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
@@ -558,7 +555,7 @@
|
||||
<div class="flex flex-col gap-3 border-t bg-muted/20 px-6 py-4">
|
||||
{#if isPending && isUploading}
|
||||
<div class="space-y-2" role="status" aria-live="polite" aria-busy="true">
|
||||
<p class="text-xs font-medium text-muted-foreground">Importando registros…</p>
|
||||
<p class="text-xs font-medium text-muted-foreground">{csvMsg('modal.importing_records')}</p>
|
||||
<div class="relative h-2 w-full overflow-hidden rounded-full bg-secondary">
|
||||
<div
|
||||
class="csv-commit-indeterminate-bar absolute top-0 h-full w-2/5 rounded-full bg-primary"
|
||||
@@ -574,7 +571,7 @@
|
||||
disabled={isUploading}
|
||||
class="text-muted-foreground hover:bg-muted/50"
|
||||
>
|
||||
Cancelar Operación
|
||||
{csvMsg('modal.cancel_operation')}
|
||||
</Button>
|
||||
<Button
|
||||
onclick={onConfirm}
|
||||
@@ -583,14 +580,14 @@
|
||||
>
|
||||
{#if isUploading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Procesando...
|
||||
{csvMsg('modal.processing')}
|
||||
{:else}
|
||||
<UploadCloud class="mr-2 h-4 w-4" />
|
||||
Confirmar Carga
|
||||
{csvMsg('modal.confirm_load')}
|
||||
{/if}
|
||||
</Button>
|
||||
{:else if isFinished}
|
||||
<Button variant="outline" onclick={onClose} class="min-w-[100px]">Cerrar</Button>
|
||||
<Button variant="outline" onclick={onClose} class="min-w-[100px]">{csvMsg('modal.close')}</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,21 @@
|
||||
import { UploadCloud, Lock } from 'lucide-svelte';
|
||||
import { cn } from '$lib/utils';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { browser } from '$app/environment';
|
||||
import { api } from '$lib/api';
|
||||
import { cookieName, getLocale } from '$lib/paraglide/runtime';
|
||||
import { csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
/** Misma fuente que el switch de idioma (cookie Paraglide); `getLocale()` puede quedar desincronizado. */
|
||||
function csvTemplateLocale(): 'es' | 'en' {
|
||||
if (browser) {
|
||||
const cookies = document.cookie.split(';').map((c) => c.trim());
|
||||
const localeCookie = cookies.find((c) => c.startsWith(`${cookieName}=`));
|
||||
const current = localeCookie ? localeCookie.split('=')[1] : '';
|
||||
if (current) return current.toLowerCase().startsWith('en') ? 'en' : 'es';
|
||||
}
|
||||
return String(getLocale()).toLowerCase().startsWith('en') ? 'en' : 'es';
|
||||
}
|
||||
|
||||
let {
|
||||
items,
|
||||
@@ -62,7 +76,7 @@
|
||||
const isValidExtension = file.name.toLowerCase().endsWith('.csv');
|
||||
|
||||
if (!isValidExtension) {
|
||||
toast.error('Formato inválido. Solo se permiten archivos .csv');
|
||||
toast.error(csvMsg('toast.invalid_csv'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,8 +120,12 @@
|
||||
if (!item.templateId) return;
|
||||
|
||||
try {
|
||||
toast.info(`Descargando plantilla para ${item.title}...`);
|
||||
const { blob, filename } = await api.getCsvTemplateDownload(item.templateId);
|
||||
const itemLabel = csvMsg(`items.${item.id}`);
|
||||
toast.info(`${csvMsg('toast.download_loading')} ${itemLabel}`);
|
||||
const { blob, filename } = await api.getCsvTemplateDownload(
|
||||
item.templateId,
|
||||
csvTemplateLocale()
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
@@ -116,9 +134,9 @@
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success(`Plantilla descargada: ${filename}`);
|
||||
toast.success(`${csvMsg('toast.download_ok')} ${filename}`);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Error al descargar la plantilla');
|
||||
toast.error(err instanceof Error ? err.message : csvMsg('toast.download_err'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -128,6 +146,10 @@
|
||||
class:opacity-60={gridLocked}
|
||||
aria-busy={gridLocked ? true : undefined}
|
||||
>
|
||||
{#snippet cardTitle(itemId: string)}
|
||||
<div class="font-medium text-sm text-balance">{csvMsg(`items.${itemId}`)}</div>
|
||||
{/snippet}
|
||||
|
||||
{#if groupedItems.ungrouped.length > 0}
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{#each groupedItems.ungrouped as item}
|
||||
@@ -171,7 +193,7 @@
|
||||
<span
|
||||
class="bg-muted px-2 py-1 rounded text-xs font-semibold text-muted-foreground border flex items-center gap-1"
|
||||
>
|
||||
<Lock class="h-3 w-3" /> Próximamente
|
||||
<Lock class="h-3 w-3" /> {csvMsg('soon')}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -183,7 +205,7 @@
|
||||
<div class="animate-bounce">
|
||||
<UploadCloud class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-primary">¡Suelta el archivo!</span>
|
||||
<span class="text-sm font-semibold text-primary">{csvMsg('drop_here')}</span>
|
||||
{:else}
|
||||
<div
|
||||
class={cn(
|
||||
@@ -193,7 +215,7 @@
|
||||
>
|
||||
<item.icon class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div class="font-medium text-sm text-balance">{item.title}</div>
|
||||
{@render cardTitle(item.id)}
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -205,7 +227,7 @@
|
||||
{#each Object.entries(groupedItems.groups) as [groupName, groupItems]}
|
||||
<div class="flex flex-col gap-3">
|
||||
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wider pl-1">
|
||||
{groupName}
|
||||
{csvMsg(`groups.${groupName}`)}
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{#each groupItems as item}
|
||||
@@ -251,7 +273,7 @@
|
||||
<span
|
||||
class="bg-muted px-2 py-1 rounded text-xs font-semibold text-muted-foreground border flex items-center gap-1"
|
||||
>
|
||||
<Lock class="h-3 w-3" /> Próximamente
|
||||
<Lock class="h-3 w-3" /> {csvMsg('soon')}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -263,7 +285,7 @@
|
||||
<div class="animate-bounce">
|
||||
<UploadCloud class="h-8 w-8 text-primary" />
|
||||
</div>
|
||||
<span class="text-sm font-semibold text-primary">¡Suelta el archivo!</span>
|
||||
<span class="text-sm font-semibold text-primary">{csvMsg('drop_here')}</span>
|
||||
{:else}
|
||||
<div
|
||||
class={cn(
|
||||
@@ -273,7 +295,7 @@
|
||||
>
|
||||
<item.icon class="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div class="font-medium text-sm text-balance">{item.title}</div>
|
||||
{@render cardTitle(item.id)}
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -91,6 +91,18 @@ export function getSidebarData(): SidebarData {
|
||||
items: [],
|
||||
permission: 'audit_logs.view',
|
||||
},
|
||||
{
|
||||
title: m["sidebar.bulk_upload.title"](),
|
||||
url: "#",
|
||||
icon: ArrowUpFromLine,
|
||||
items: [
|
||||
{
|
||||
title: m["sidebar.bulk_upload.entry"](),
|
||||
url: "/dashboard/csv-upload",
|
||||
permission: "csv_upload.process",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
title: m["sidebar.reference_data.title"](),
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
|
||||
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
||||
import FolderIcon from '@lucide/svelte/icons/folder';
|
||||
import ForwardIcon from '@lucide/svelte/icons/forward';
|
||||
import Trash2Icon from '@lucide/svelte/icons/trash-2';
|
||||
import { helpStore } from '$lib/stores/help.svelte';
|
||||
import { m } from '$lib/i18n/messages';
|
||||
|
||||
let {
|
||||
projects
|
||||
@@ -15,28 +8,14 @@
|
||||
projects: {
|
||||
name: string;
|
||||
url: string;
|
||||
// This should be `Component` after @lucide/svelte updates types
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
icon: any;
|
||||
}[];
|
||||
} = $props();
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
let open = $state(false);
|
||||
let position = $state({ x: 0, y: 0 });
|
||||
|
||||
async function handleMoreClick(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
open = false;
|
||||
position = { x: e.clientX, y: e.clientY };
|
||||
await tick();
|
||||
open = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Sidebar.Group class="group-data-[collapsible=icon]:hidden">
|
||||
<Sidebar.GroupLabel>Gestion</Sidebar.GroupLabel>
|
||||
<Sidebar.GroupLabel>{m['sidebar.management_label']()}</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each projects as item (item.name)}
|
||||
<Sidebar.MenuItem>
|
||||
@@ -48,58 +27,7 @@
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuButton>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuAction showOnHover {...props}>
|
||||
<EllipsisIcon />
|
||||
<span class="sr-only">More</span>
|
||||
</Sidebar.MenuAction>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
class="w-48 rounded-lg"
|
||||
side={sidebar.isMobile ? 'bottom' : 'right'}
|
||||
align={sidebar.isMobile ? 'end' : 'start'}
|
||||
>
|
||||
<DropdownMenu.Item>
|
||||
<FolderIcon class="text-muted-foreground" />
|
||||
<span>View Project</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item>
|
||||
<ForwardIcon class="text-muted-foreground" />
|
||||
<span>Share Project</span>
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item>
|
||||
<Trash2Icon class="text-muted-foreground" />
|
||||
<span>Delete Project</span>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton class="text-sidebar-foreground/70" onclick={handleMoreClick}>
|
||||
<EllipsisIcon class="text-sidebar-foreground/70" />
|
||||
<span>More</span>
|
||||
</Sidebar.MenuButton>
|
||||
|
||||
<DropdownMenu.Root open={open} onOpenChange={(v) => (open = v)}>
|
||||
<DropdownMenu.Trigger
|
||||
class="fixed z-50 size-0"
|
||||
style="top: {position.y}px; left: {position.x}px"
|
||||
/>
|
||||
<DropdownMenu.Content class="w-48 rounded-lg" side="right" align="start">
|
||||
<DropdownMenu.Item>
|
||||
<a href="/dashboard/csv-upload" class="flex w-full items-center gap-2">
|
||||
<FolderIcon class="size-4 text-muted-foreground" />
|
||||
<span>Carga CSV</span>
|
||||
</a>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
|
||||
@@ -22,9 +22,10 @@ import {
|
||||
|
||||
export interface CsvUploadItem {
|
||||
id: string;
|
||||
title: string;
|
||||
/** Etiqueta vía i18n: csv_upload.items.<id> */
|
||||
icon: any;
|
||||
group?: string; // For grouping within a tab
|
||||
/** Clave i18n csv_upload.groups.<group> */
|
||||
group?: string;
|
||||
modelTarget?: string; // The backend model this maps to
|
||||
description?: string;
|
||||
/** Backend template id for CSV download (e.g. customs_brokers, part_numbers). No physical file. */
|
||||
@@ -36,9 +37,10 @@ export interface CsvUploadItem {
|
||||
|
||||
export interface CsvUploadField {
|
||||
name: string;
|
||||
label: string;
|
||||
/** Ruta bajo csv_upload.* (p. ej. params.load_mode) */
|
||||
labelKey: string;
|
||||
type: 'text' | 'select' | 'boolean' | 'date' | 'radio';
|
||||
options?: { label: string; value: string | boolean | number }[];
|
||||
options?: { labelKey: string; value: string | boolean | number }[];
|
||||
required?: boolean;
|
||||
defaultValue?: any;
|
||||
}
|
||||
@@ -46,9 +48,9 @@ export interface CsvUploadField {
|
||||
/** Global parameters shown in the CSV upload footer bar (one option or the other via select). */
|
||||
export interface GlobalCsvParam {
|
||||
name: string;
|
||||
label: string;
|
||||
labelKey: string;
|
||||
type: 'select';
|
||||
options: { label: string; value: string }[];
|
||||
options: { labelKey: string; value: string }[];
|
||||
defaultValue: string;
|
||||
}
|
||||
|
||||
@@ -56,62 +58,62 @@ export interface GlobalCsvParam {
|
||||
export const globalCsvParams: GlobalCsvParam[] = [
|
||||
{
|
||||
name: 'mode',
|
||||
label: 'Modo de Carga',
|
||||
labelKey: 'params.load_mode',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Actualizar', value: 'update' },
|
||||
{ label: 'Reemplazar', value: 'replace' }
|
||||
{ labelKey: 'options.update', value: 'update' },
|
||||
{ labelKey: 'options.replace', value: 'replace' }
|
||||
],
|
||||
defaultValue: 'update'
|
||||
},
|
||||
{
|
||||
name: 'dateFormat',
|
||||
label: 'Formato de Fecha',
|
||||
labelKey: 'params.date_format',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
|
||||
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
|
||||
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
|
||||
{ labelKey: 'options.date_dd_mm', value: 'dd/mm/yyyy' },
|
||||
{ labelKey: 'options.date_mm_dd', value: 'mm/dd/yyyy' },
|
||||
{ labelKey: 'options.date_iso', value: 'yyyy-mm-dd' }
|
||||
],
|
||||
defaultValue: 'dd/mm/yyyy'
|
||||
},
|
||||
{
|
||||
name: 'weight_unit',
|
||||
label: 'Unidad de Peso',
|
||||
labelKey: 'params.weight_unit',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Kilos (Kgs)', value: 'kgs' },
|
||||
{ label: 'Libras (Lbs)', value: 'lbs' }
|
||||
{ labelKey: 'options.kgs', value: 'kgs' },
|
||||
{ labelKey: 'options.lbs', value: 'lbs' }
|
||||
],
|
||||
defaultValue: 'kgs'
|
||||
},
|
||||
{
|
||||
name: 'autonumber_series',
|
||||
label: 'Autonumerar Partidas/Series',
|
||||
labelKey: 'params.autonumber_series',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Sí', value: 'true' },
|
||||
{ label: 'No', value: 'false' }
|
||||
{ labelKey: 'options.yes', value: 'true' },
|
||||
{ labelKey: 'options.no', value: 'false' }
|
||||
],
|
||||
defaultValue: 'false'
|
||||
},
|
||||
{
|
||||
name: 'load_subpartidas',
|
||||
label: 'Levantar Subpartidas',
|
||||
labelKey: 'params.load_subpartidas',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Sí', value: 'true' },
|
||||
{ label: 'No', value: 'false' }
|
||||
{ labelKey: 'options.yes', value: 'true' },
|
||||
{ labelKey: 'options.no', value: 'false' }
|
||||
],
|
||||
defaultValue: 'false'
|
||||
},
|
||||
{
|
||||
name: 'recalculate_pedimento_date',
|
||||
label: 'Recalcular Fecha Pedimento',
|
||||
labelKey: 'params.recalculate_pedimento_date',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'Sí', value: 'true' },
|
||||
{ label: 'No', value: 'false' }
|
||||
{ labelKey: 'options.yes', value: 'true' },
|
||||
{ labelKey: 'options.no', value: 'false' }
|
||||
],
|
||||
defaultValue: 'false'
|
||||
}
|
||||
@@ -122,11 +124,11 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
catalogos: [
|
||||
{
|
||||
name: 'mode',
|
||||
label: 'Modo de Carga',
|
||||
labelKey: 'params.load_mode',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{ label: 'Actualizar', value: 'update' },
|
||||
{ label: 'Reemplazar', value: 'replace' }
|
||||
{ labelKey: 'options.update', value: 'update' },
|
||||
{ labelKey: 'options.replace', value: 'replace' }
|
||||
],
|
||||
defaultValue: 'update'
|
||||
}
|
||||
@@ -134,11 +136,11 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
transportes: [
|
||||
{
|
||||
name: 'mode',
|
||||
label: 'Modo de Carga',
|
||||
labelKey: 'params.load_mode',
|
||||
type: 'radio',
|
||||
options: [
|
||||
{ label: 'Actualizar', value: 'update' },
|
||||
{ label: 'Reemplazar', value: 'replace' }
|
||||
{ labelKey: 'options.update', value: 'update' },
|
||||
{ labelKey: 'options.replace', value: 'replace' }
|
||||
],
|
||||
defaultValue: 'update'
|
||||
}
|
||||
@@ -146,24 +148,24 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
importacion: [
|
||||
{
|
||||
name: 'autonumber_remesas',
|
||||
label: 'Autonumerar Remesas',
|
||||
labelKey: 'params.autonumber_remesas',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
name: 'recalculate_dates',
|
||||
label: 'Recalcular Fechas',
|
||||
labelKey: 'params.recalculate_dates',
|
||||
type: 'boolean',
|
||||
defaultValue: false
|
||||
},
|
||||
{
|
||||
name: 'dateFormat',
|
||||
label: 'Formato de Fecha',
|
||||
labelKey: 'params.date_format',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
|
||||
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
|
||||
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
|
||||
{ labelKey: 'options.date_dd_mm', value: 'dd/mm/yyyy' },
|
||||
{ labelKey: 'options.date_mm_dd', value: 'mm/dd/yyyy' },
|
||||
{ labelKey: 'options.date_iso', value: 'yyyy-mm-dd' }
|
||||
],
|
||||
defaultValue: 'dd/mm/yyyy'
|
||||
}
|
||||
@@ -171,28 +173,28 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
exportacion: [
|
||||
{
|
||||
name: 'invoice_type',
|
||||
label: 'Tipo de Factura',
|
||||
labelKey: 'params.invoice_type',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'AFIJO', value: 'AFIJO' },
|
||||
{ label: 'NORMAL', value: 'NORMAL' },
|
||||
{ labelKey: 'options.afi', value: 'AFIJO' },
|
||||
{ labelKey: 'options.normal', value: 'NORMAL' },
|
||||
],
|
||||
defaultValue: 'AFIJO',
|
||||
},
|
||||
{
|
||||
name: 'is_regime_change',
|
||||
label: 'Es Cambio de Régimen',
|
||||
labelKey: 'params.is_regime_change',
|
||||
type: 'boolean',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: 'dateFormat',
|
||||
label: 'Formato de Fecha',
|
||||
labelKey: 'params.date_format',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'DD/MM/YYYY', value: 'dd/mm/yyyy' },
|
||||
{ label: 'MM/DD/YYYY', value: 'mm/dd/yyyy' },
|
||||
{ label: 'YYYY-MM-DD', value: 'yyyy-mm-dd' }
|
||||
{ labelKey: 'options.date_dd_mm', value: 'dd/mm/yyyy' },
|
||||
{ labelKey: 'options.date_mm_dd', value: 'mm/dd/yyyy' },
|
||||
{ labelKey: 'options.date_iso', value: 'yyyy-mm-dd' }
|
||||
],
|
||||
defaultValue: 'dd/mm/yyyy'
|
||||
}
|
||||
@@ -204,7 +206,6 @@ export const tabSettings: Record<string, CsvUploadField[]> = {
|
||||
export const catalogosConfig: CsvUploadItem[] = [
|
||||
{
|
||||
id: 'customs_brokers',
|
||||
title: 'Agentes Aduanales',
|
||||
icon: User,
|
||||
modelTarget: 'CustomsBroker',
|
||||
templateId: 'customs_brokers',
|
||||
@@ -212,7 +213,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'clients_providers',
|
||||
title: 'Clientes y Proveedores',
|
||||
icon: Users,
|
||||
modelTarget: 'ClientProvider',
|
||||
templateId: 'clients_providers',
|
||||
@@ -220,7 +220,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'exchange_rates',
|
||||
title: 'Tipo de Cambios',
|
||||
icon: DollarSign,
|
||||
modelTarget: 'ExchangeRate',
|
||||
templateId: 'exchange_rates',
|
||||
@@ -228,7 +227,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'material_classes',
|
||||
title: 'Clases de Materiales',
|
||||
icon: Package,
|
||||
modelTarget: 'MaterialClass',
|
||||
templateId: 'material_classes',
|
||||
@@ -236,7 +234,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'part_numbers',
|
||||
title: 'Números de parte',
|
||||
icon: Hash,
|
||||
modelTarget: 'Part',
|
||||
templateId: 'part_numbers',
|
||||
@@ -244,7 +241,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'boms',
|
||||
title: 'BOMs',
|
||||
icon: Briefcase,
|
||||
modelTarget: 'Bom',
|
||||
templateId: 'boms',
|
||||
@@ -253,30 +249,26 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'items',
|
||||
title: 'Partidas (Permisos)',
|
||||
icon: FileText,
|
||||
group: 'Permisos',
|
||||
group: 'permisos',
|
||||
modelTarget: 'ItemPermission',
|
||||
templateId: 'part_numbers'
|
||||
},
|
||||
{
|
||||
id: 'headers',
|
||||
title: 'Encabezados (Permisos)',
|
||||
icon: FileText,
|
||||
group: 'Permisos',
|
||||
group: 'permisos',
|
||||
modelTarget: 'HeaderPermission',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'historical_fractions',
|
||||
title: 'Fracciones Históricas',
|
||||
icon: Calendar,
|
||||
modelTarget: 'HistoricalFraction',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'pedimentos',
|
||||
title: 'Pedimentos',
|
||||
icon: FileDigit,
|
||||
modelTarget: 'Pedimento',
|
||||
templateId: 'pedimentos',
|
||||
@@ -288,7 +280,6 @@ export const catalogosConfig: CsvUploadItem[] = [
|
||||
export const transportesConfig: CsvUploadItem[] = [
|
||||
{
|
||||
id: 'transporters',
|
||||
title: 'Transportistas',
|
||||
icon: Ship,
|
||||
modelTarget: 'Transporter',
|
||||
templateId: 'transporters',
|
||||
@@ -296,7 +287,6 @@ export const transportesConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'transports',
|
||||
title: 'Transportes',
|
||||
icon: Truck,
|
||||
modelTarget: 'Transport',
|
||||
templateId: 'transports',
|
||||
@@ -304,7 +294,6 @@ export const transportesConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'drivers',
|
||||
title: 'Conductores',
|
||||
icon: User,
|
||||
modelTarget: 'Driver',
|
||||
templateId: 'drivers',
|
||||
@@ -312,7 +301,6 @@ export const transportesConfig: CsvUploadItem[] = [
|
||||
},
|
||||
{
|
||||
id: 'trailers',
|
||||
title: 'Trailers y Cajas',
|
||||
icon: Container,
|
||||
modelTarget: 'Trailer',
|
||||
templateId: 'trailers',
|
||||
@@ -326,27 +314,24 @@ export const importacionConfig: CsvUploadItem[] = [
|
||||
// Impo Temp
|
||||
{
|
||||
id: 'imp_temp_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Impo. Temp.',
|
||||
group: 'impo_temp',
|
||||
modelTarget: 'invoice_header',
|
||||
templateId: 'imp_temp_header',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'imp_temp_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Impo. Temp.',
|
||||
group: 'impo_temp',
|
||||
modelTarget: 'invoice_details',
|
||||
templateId: 'imp_temp_details',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'imp_temp_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Impo. Temp.',
|
||||
group: 'impo_temp',
|
||||
modelTarget: 'invoice_series',
|
||||
templateId: 'imp_temp_series',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
@@ -354,27 +339,24 @@ export const importacionConfig: CsvUploadItem[] = [
|
||||
// Impo Def
|
||||
{
|
||||
id: 'imp_def_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Impo. Def.',
|
||||
group: 'impo_def',
|
||||
modelTarget: 'invoice_header',
|
||||
templateId: 'imp_def_header',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'imp_def_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Impo. Def.',
|
||||
group: 'impo_def',
|
||||
modelTarget: 'invoice_details',
|
||||
templateId: 'imp_def_details',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'imp_def_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Impo. Def.',
|
||||
group: 'impo_def',
|
||||
modelTarget: 'invoice_series',
|
||||
templateId: 'imp_def_series',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
@@ -382,27 +364,24 @@ export const importacionConfig: CsvUploadItem[] = [
|
||||
// Compras Mex
|
||||
{
|
||||
id: 'comp_mex_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Compras Mex.',
|
||||
group: 'cmex',
|
||||
modelTarget: 'invoice_header',
|
||||
templateId: 'cmex_header',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'comp_mex_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Compras Mex.',
|
||||
group: 'cmex',
|
||||
modelTarget: 'invoice_details',
|
||||
templateId: 'cmex_details',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
},
|
||||
{
|
||||
id: 'comp_mex_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Compras Mex.',
|
||||
group: 'cmex',
|
||||
modelTarget: 'invoice_series',
|
||||
templateId: 'cmex_series',
|
||||
layoutModule: 'layouts_csv/facturas'
|
||||
@@ -415,70 +394,62 @@ export const exportacionConfig: CsvUploadItem[] = [
|
||||
// Expo Def / Cam. Reg.
|
||||
{
|
||||
id: 'exp_def_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Expo. Def./Cam. Reg.',
|
||||
group: 'expo_def',
|
||||
modelTarget: 'invoice_header',
|
||||
templateId: 'exp_def_header',
|
||||
layoutModule: 'layouts_csv/exportacion'
|
||||
},
|
||||
{
|
||||
id: 'exp_def_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Expo. Def./Cam. Reg.',
|
||||
group: 'expo_def',
|
||||
modelTarget: 'invoice_details',
|
||||
templateId: 'exp_def_details',
|
||||
layoutModule: 'layouts_csv/exportacion'
|
||||
},
|
||||
{
|
||||
id: 'exp_def_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Expo. Def./Cam. Reg.',
|
||||
group: 'expo_def',
|
||||
modelTarget: 'invoice_series',
|
||||
templateId: 'exp_def_series',
|
||||
layoutModule: 'layouts_csv/exportacion'
|
||||
},
|
||||
{
|
||||
id: 'exp_def_nodes',
|
||||
title: 'NODES',
|
||||
icon: Briefcase,
|
||||
group: 'Expo. Def./Cam. Reg.',
|
||||
group: 'expo_def',
|
||||
modelTarget: 'Nodes',
|
||||
disabled: true,
|
||||
},
|
||||
// Expo Rep
|
||||
{
|
||||
id: 'exp_rep_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Expo. Rep.',
|
||||
group: 'expo_rep',
|
||||
modelTarget: 'InvoiceHeader',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'exp_rep_details',
|
||||
title: 'Partidas',
|
||||
icon: Package,
|
||||
group: 'Expo. Rep.',
|
||||
group: 'expo_rep',
|
||||
modelTarget: 'InvoiceSalesDetails',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 'exp_rep_series',
|
||||
title: 'Series',
|
||||
icon: Hash,
|
||||
group: 'Expo. Rep.',
|
||||
group: 'expo_rep',
|
||||
modelTarget: 'InvoiceSeries',
|
||||
disabled: true,
|
||||
},
|
||||
// Manifiesto
|
||||
{
|
||||
id: 'manifest_header',
|
||||
title: 'Encabezado',
|
||||
icon: FileText,
|
||||
group: 'Manifiesto',
|
||||
group: 'manifest',
|
||||
modelTarget: 'Manifest',
|
||||
disabled: true,
|
||||
},
|
||||
|
||||
46
frontend/src/lib/i18n/csv-msg.ts
Normal file
46
frontend/src/lib/i18n/csv-msg.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Textos CSV desde `csv-upload-messages.{en,es}.json` (rama `csv_upload` extraída de messages/).
|
||||
* Mantener en sync al editar `messages/*.json` (p. ej. volver a exportar la clave csv_upload).
|
||||
*/
|
||||
import enPack from './csv-upload-messages.en.json';
|
||||
import esPack from './csv-upload-messages.es.json';
|
||||
import { baseLocale, getLocale } from '$lib/paraglide/runtime';
|
||||
|
||||
type CsvUploadPack = Record<string, unknown>;
|
||||
|
||||
function pickPack(): CsvUploadPack {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = String(getLocale()).toLowerCase();
|
||||
} catch {
|
||||
raw = String(baseLocale).toLowerCase();
|
||||
}
|
||||
const root = raw.startsWith('en') ? enPack : esPack;
|
||||
return root as CsvUploadPack;
|
||||
}
|
||||
|
||||
function walk(root: Record<string, unknown>, keys: string[]): unknown {
|
||||
let cur: unknown = root;
|
||||
for (const k of keys) {
|
||||
if (cur === null || typeof cur !== 'object') return undefined;
|
||||
cur = (cur as Record<string, unknown>)[k];
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/** Navega `csv_upload.a.b.c` a partir de `subPath` = `a.b.c`. */
|
||||
export function csvMsg(subPath: string): string {
|
||||
const pack = pickPack();
|
||||
if (!pack) return subPath;
|
||||
const v = walk(pack as Record<string, unknown>, subPath.split('.'));
|
||||
return typeof v === 'string' ? v : subPath;
|
||||
}
|
||||
|
||||
/** Sustituye `{clave}` en el string del mensaje. */
|
||||
export function csvFmt(subPath: string, vars: Record<string, string | number> = {}): string {
|
||||
let s = csvMsg(subPath);
|
||||
for (const [k, val] of Object.entries(vars)) {
|
||||
s = s.replaceAll(`{${k}}`, String(val));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
193
frontend/src/lib/i18n/csv-upload-messages.en.json
Normal file
193
frontend/src/lib/i18n/csv-upload-messages.en.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"page_title": "CSV import",
|
||||
"intro_help": "Left-click: upload CSV file. Right-click: download template.",
|
||||
"tab_catalogos": "Catalogs",
|
||||
"tab_transportes": "Transportation",
|
||||
"tab_importacion": "Import",
|
||||
"tab_exportacion": "Export",
|
||||
"section_catalogs": "General Catalogs",
|
||||
"section_transport": "Transportation",
|
||||
"section_import": "Import operations",
|
||||
"section_export": "Export operations",
|
||||
"params_header": "Global parameters",
|
||||
"config_prefix": "Settings",
|
||||
"soon": "Coming soon",
|
||||
"drop_here": "Drop the file!",
|
||||
"groups": {
|
||||
"permisos": "Permissions",
|
||||
"impo_temp": "Temporary import",
|
||||
"impo_def": "Definitive import",
|
||||
"cmex": "Mexican purchases",
|
||||
"expo_def": "Definitive export / regime change",
|
||||
"expo_rep": "Export replenishment",
|
||||
"manifest": "Manifest"
|
||||
},
|
||||
"items": {
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"clients_providers": "Clients and Providers",
|
||||
"exchange_rates": "Exchange Rates",
|
||||
"material_classes": "Classes",
|
||||
"part_numbers": "Parts",
|
||||
"boms": "BOMs",
|
||||
"items": "Lines (permissions)",
|
||||
"headers": "Headers (permissions)",
|
||||
"historical_fractions": "Historical tariff fractions",
|
||||
"pedimentos": "Pedimentos",
|
||||
"transporters": "Carriers",
|
||||
"transports": "Vehicles",
|
||||
"drivers": "Drivers",
|
||||
"trailers": "Trailers",
|
||||
"imp_temp_header": "Header",
|
||||
"imp_temp_details": "Lines",
|
||||
"imp_temp_series": "Serial numbers",
|
||||
"imp_def_header": "Header",
|
||||
"imp_def_details": "Lines",
|
||||
"imp_def_series": "Serial numbers",
|
||||
"comp_mex_header": "Header",
|
||||
"comp_mex_details": "Lines",
|
||||
"comp_mex_series": "Serial numbers",
|
||||
"exp_def_header": "Header",
|
||||
"exp_def_details": "Lines",
|
||||
"exp_def_series": "Serial numbers",
|
||||
"exp_def_nodes": "NODES",
|
||||
"exp_rep_header": "Header",
|
||||
"exp_rep_details": "Lines",
|
||||
"exp_rep_series": "Serial numbers",
|
||||
"manifest_header": "Header"
|
||||
},
|
||||
"params": {
|
||||
"load_mode": "Load mode",
|
||||
"date_format": "Date format",
|
||||
"weight_unit": "Weight unit",
|
||||
"autonumber_series": "Autonumber lines/series",
|
||||
"load_subpartidas": "Load sub-lines",
|
||||
"recalculate_pedimento_date": "Recalculate pedimento date",
|
||||
"autonumber_remesas": "Autonumber consignments",
|
||||
"recalculate_dates": "Recalculate dates",
|
||||
"invoice_type": "Invoice type",
|
||||
"is_regime_change": "Regime change"
|
||||
},
|
||||
"options": {
|
||||
"update": "Update",
|
||||
"replace": "Replace",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"kgs": "Kilograms (kg)",
|
||||
"lbs": "Pounds (lb)",
|
||||
"date_dd_mm": "DD/MM/YYYY",
|
||||
"date_mm_dd": "MM/DD/YYYY",
|
||||
"date_iso": "YYYY-MM-DD",
|
||||
"afi": "AFIJO",
|
||||
"normal": "NORMAL"
|
||||
},
|
||||
"progress": {
|
||||
"upload": "Uploading CSV file",
|
||||
"scan": "Validating records on the server",
|
||||
"commit": "Saving records to the database",
|
||||
"upload_known": "Uploading file…",
|
||||
"upload_unknown": "Uploading file (unknown size in browser)…",
|
||||
"in_progress": "In progress…",
|
||||
"resume_hint": "Resuming import saved in this tab…",
|
||||
"rows_file": "File: ~{n} data row(s) — uploading (not yet validated on server)…",
|
||||
"rows_scan": "Records processed: {current} / {total}",
|
||||
"rows_commit": "Records saved: {current} / {total}",
|
||||
"rows_commit_fallback": "Saving to database… ({current} / {total} using last known total)"
|
||||
},
|
||||
"toast": {
|
||||
"invalid_csv": "Invalid format. Only .csv files are allowed.",
|
||||
"download_loading": "Downloading template…",
|
||||
"download_ok": "Template downloaded.",
|
||||
"download_err": "Could not download the template.",
|
||||
"upload_err": "Could not upload the file.",
|
||||
"upload_err_generic": "Unexpected error uploading the file.",
|
||||
"scan_done": "Scan complete. Review the results.",
|
||||
"import_done": "Import completed. Review the record list.",
|
||||
"import_maybe_done": "Import may have completed. Review the record list.",
|
||||
"stale_job": "This import is no longer available (session expired or job removed). You can start a new upload.",
|
||||
"poll_err": "Could not fetch status",
|
||||
"commit_err": "Could not start import",
|
||||
"scan_alt": "Scan finished. If you do not see the modal, check the record list.",
|
||||
"finished_none": "No records inserted. Review the errors below.",
|
||||
"commit_warning_ok": "{inserted} inserted, {updated} updated. {skipped} rejected.",
|
||||
"commit_warning_none": "No records inserted or updated. {skipped} rejected.",
|
||||
"success_counts": "Import completed: {msg}",
|
||||
"warn_skipped": "{n} records rejected or skipped",
|
||||
"error_processing": "Processing error: {msg}",
|
||||
"n_inserted": "{n} inserted",
|
||||
"n_updated": "{n} updated",
|
||||
"err_fetch_scan_result": "Could not fetch the scan result. Check the results modal.",
|
||||
"err_unknown": "Unknown error",
|
||||
"err_processing_fallback": "Processing error. Check the modal or details."
|
||||
},
|
||||
"pending": {
|
||||
"badge": "Pending",
|
||||
"title": "Imports pending confirmation",
|
||||
"description": "Scans ready to save to the database. Expired jobs disappear when you refresh.",
|
||||
"refresh": "Refresh",
|
||||
"empty": "No pending imports for this company.",
|
||||
"checking": "Checking with the server…",
|
||||
"total_rows": "Total rows",
|
||||
"valid_rows": "Valid",
|
||||
"resume": "Resume",
|
||||
"remove": "Remove",
|
||||
"profiles": {
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"clients_providers": "Clients and Providers",
|
||||
"exchange_rates": "Exchange Rates",
|
||||
"pedimentos": "Pedimentos",
|
||||
"material_classes": "Classes",
|
||||
"vehicles": "Vehicles",
|
||||
"drivers": "Drivers",
|
||||
"trailers": "Trailers",
|
||||
"transporters": "Carriers",
|
||||
"part_numbers": "Parts",
|
||||
"boms": "BOMs",
|
||||
"exportacion": "Export operations",
|
||||
"imports": "Import operations"
|
||||
}
|
||||
},
|
||||
"config_empty": "No module-specific settings.",
|
||||
"modal": {
|
||||
"title_pending": "Import validation",
|
||||
"title_success": "Import successful",
|
||||
"title_warning": "Import with remarks",
|
||||
"desc_pending": "Review the preliminary analysis before confirming.",
|
||||
"desc_done": "The import process has finished.",
|
||||
"total_rows": "Total rows",
|
||||
"valid_rows": "Valid",
|
||||
"invalid_rows": "Invalid",
|
||||
"errors": "Errors",
|
||||
"errors_heading": "Scan errors (fix in your CSV)",
|
||||
"errors_badge": "{shown} of {total} error(s)",
|
||||
"errors_truncated": "Download the CSV to see all errors.",
|
||||
"errors_missing_detail": "{count} row(s) had errors but details are not available. Ensure the server is up to date and upload again.",
|
||||
"scan_ok_title": "File validated successfully",
|
||||
"scan_ok_body": "All rows look correct and ready to import.",
|
||||
"scan_problems_title": "Problems found in the file",
|
||||
"scan_problems_body": "Fix the issues listed below in your CSV and upload again, or confirm to import only valid rows (invalid rows will be skipped).",
|
||||
"inserted": "Inserted",
|
||||
"updated": "Updated",
|
||||
"rejected": "Rejected",
|
||||
"rejected_hint": "See line-by-line detail in the table below.",
|
||||
"ref_gaps_title": "Reference gaps (FK / catalogs)",
|
||||
"ref_gaps_body": "There are {n} critical reference gap(s). Review catalogs and rejected rows before retrying.",
|
||||
"ref_state_title": "Reference state",
|
||||
"ref_state_ok": "References ready to operate (no critical gaps reported).",
|
||||
"ref_state_other": "No numeric gaps; review the server message if applicable.",
|
||||
"skipped_reasons_heading": "Rejection reasons summary",
|
||||
"commit_errors_heading": "Error detail",
|
||||
"rows_badge": "{n} rows",
|
||||
"importing_records": "Importing records…",
|
||||
"cancel_operation": "Cancel",
|
||||
"processing": "Processing…",
|
||||
"confirm_load": "Confirm import",
|
||||
"close": "Close",
|
||||
"th_line": "Line",
|
||||
"th_column": "Column",
|
||||
"th_message": "Message",
|
||||
"th_solution": "Solution",
|
||||
"th_reference": "Reference",
|
||||
"th_reason": "Reason",
|
||||
"download_csv": "Download CSV"
|
||||
}
|
||||
}
|
||||
193
frontend/src/lib/i18n/csv-upload-messages.es.json
Normal file
193
frontend/src/lib/i18n/csv-upload-messages.es.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"page_title": "Importación CSV",
|
||||
"intro_help": "Clic izquierdo: cargar archivo CSV. Clic derecho: descargar plantilla.",
|
||||
"tab_catalogos": "Catálogos",
|
||||
"tab_transportes": "Transportes",
|
||||
"tab_importacion": "Importación",
|
||||
"tab_exportacion": "Exportación",
|
||||
"section_catalogs": "Catalogos Generales",
|
||||
"section_transport": "Transportes",
|
||||
"section_import": "Operaciones de importación",
|
||||
"section_export": "Operaciones de exportación",
|
||||
"params_header": "Parámetros globales",
|
||||
"config_prefix": "Configuración",
|
||||
"soon": "Próximamente",
|
||||
"drop_here": "¡Suelta el archivo!",
|
||||
"groups": {
|
||||
"permisos": "Permisos",
|
||||
"impo_temp": "Impo. temp.",
|
||||
"impo_def": "Impo. def.",
|
||||
"cmex": "Compras mex.",
|
||||
"expo_def": "Expo. def./Cam. reg.",
|
||||
"expo_rep": "Expo. rep.",
|
||||
"manifest": "Manifiesto"
|
||||
},
|
||||
"items": {
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"clients_providers": "Clientes y Proveedores",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
"material_classes": "Clases",
|
||||
"part_numbers": "Partes",
|
||||
"boms": "BOMs",
|
||||
"items": "Partidas (permisos)",
|
||||
"headers": "Encabezados (permisos)",
|
||||
"historical_fractions": "Fracciones históricas",
|
||||
"pedimentos": "Pedimentos",
|
||||
"transporters": "Transportistas",
|
||||
"transports": "Vehículos",
|
||||
"drivers": "Conductores",
|
||||
"trailers": "Trailers",
|
||||
"imp_temp_header": "Encabezado",
|
||||
"imp_temp_details": "Partidas",
|
||||
"imp_temp_series": "Series",
|
||||
"imp_def_header": "Encabezado",
|
||||
"imp_def_details": "Partidas",
|
||||
"imp_def_series": "Series",
|
||||
"comp_mex_header": "Encabezado",
|
||||
"comp_mex_details": "Partidas",
|
||||
"comp_mex_series": "Series",
|
||||
"exp_def_header": "Encabezado",
|
||||
"exp_def_details": "Partidas",
|
||||
"exp_def_series": "Series",
|
||||
"exp_def_nodes": "NODES",
|
||||
"exp_rep_header": "Encabezado",
|
||||
"exp_rep_details": "Partidas",
|
||||
"exp_rep_series": "Series",
|
||||
"manifest_header": "Encabezado"
|
||||
},
|
||||
"params": {
|
||||
"load_mode": "Modo de carga",
|
||||
"date_format": "Formato de fecha",
|
||||
"weight_unit": "Unidad de peso",
|
||||
"autonumber_series": "Autonumerar partidas/series",
|
||||
"load_subpartidas": "Levantar subpartidas",
|
||||
"recalculate_pedimento_date": "Recalcular fecha pedimento",
|
||||
"autonumber_remesas": "Autonumerar remesas",
|
||||
"recalculate_dates": "Recalcular fechas",
|
||||
"invoice_type": "Tipo de factura",
|
||||
"is_regime_change": "Es cambio de régimen"
|
||||
},
|
||||
"options": {
|
||||
"update": "Actualizar",
|
||||
"replace": "Reemplazar",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"kgs": "Kilos (kg)",
|
||||
"lbs": "Libras (lb)",
|
||||
"date_dd_mm": "DD/MM/YYYY",
|
||||
"date_mm_dd": "MM/DD/YYYY",
|
||||
"date_iso": "YYYY-MM-DD",
|
||||
"afi": "AFIJO",
|
||||
"normal": "NORMAL"
|
||||
},
|
||||
"progress": {
|
||||
"upload": "Subiendo archivo CSV",
|
||||
"scan": "Validando registros en el servidor",
|
||||
"commit": "Grabando registros en base de datos",
|
||||
"upload_known": "Subiendo archivo…",
|
||||
"upload_unknown": "Subiendo archivo (tamaño desconocido en el navegador)…",
|
||||
"in_progress": "En proceso…",
|
||||
"resume_hint": "Reanudando la importación guardada en esta pestaña…",
|
||||
"rows_file": "Archivo: ~{n} fila(s) de datos — subiendo (aún no se validan registros en servidor)…",
|
||||
"rows_scan": "Registros procesados: {current} / {total}",
|
||||
"rows_commit": "Registros grabados: {current} / {total}",
|
||||
"rows_commit_fallback": "Grabando en base de datos… ({current} / {total} según último total conocido)"
|
||||
},
|
||||
"toast": {
|
||||
"invalid_csv": "Formato inválido. Solo se permiten archivos .csv",
|
||||
"download_loading": "Descargando plantilla…",
|
||||
"download_ok": "Plantilla descargada.",
|
||||
"download_err": "Error al descargar la plantilla",
|
||||
"upload_err": "Error al subir el archivo",
|
||||
"upload_err_generic": "Error inesperado al subir el archivo",
|
||||
"scan_done": "Escaneo completado. Revisa los resultados.",
|
||||
"import_done": "Importación completada. Revisa el listado de registros.",
|
||||
"import_maybe_done": "La importación pudo completarse. Revisa el listado de registros.",
|
||||
"stale_job": "Esta importación ya no está disponible (sesión expirada o trabajo eliminado). Puedes iniciar una nueva carga.",
|
||||
"poll_err": "Error al consultar el estado",
|
||||
"commit_err": "Error al iniciar la importación",
|
||||
"scan_alt": "El escaneo terminó. Si no ves el modal, revisa el listado de registros.",
|
||||
"finished_none": "No se insertaron registros. Revisa los errores a continuación.",
|
||||
"commit_warning_ok": "{inserted} insertados, {updated} actualizados. {skipped} rechazados.",
|
||||
"commit_warning_none": "No se insertaron ni actualizaron registros. {skipped} rechazados.",
|
||||
"success_counts": "Importación completada: {msg}",
|
||||
"warn_skipped": "{n} registros fueron rechazados u omitidos",
|
||||
"error_processing": "Error en el procesamiento: {msg}",
|
||||
"n_inserted": "{n} insertados",
|
||||
"n_updated": "{n} actualizados",
|
||||
"err_fetch_scan_result": "Error al obtener el resultado. Revisa el modal de resultados.",
|
||||
"err_unknown": "Error desconocido",
|
||||
"err_processing_fallback": "Error en el procesamiento. Revisa el modal o los detalles."
|
||||
},
|
||||
"pending": {
|
||||
"badge": "Pendientes",
|
||||
"title": "Importaciones pendientes de confirmar",
|
||||
"description": "Escaneos listos para insertar en base de datos. Si el trabajo ya expiró en el servidor, desaparecerán al actualizar.",
|
||||
"refresh": "Actualizar",
|
||||
"empty": "No hay importaciones pendientes para esta empresa.",
|
||||
"checking": "Comprobando con el servidor…",
|
||||
"total_rows": "Total filas",
|
||||
"valid_rows": "Válidas",
|
||||
"resume": "Reanudar",
|
||||
"remove": "Quitar",
|
||||
"profiles": {
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"clients_providers": "Clientes y Proveedores",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
"pedimentos": "Pedimentos",
|
||||
"material_classes": "Clases",
|
||||
"vehicles": "Vehículos",
|
||||
"drivers": "Conductores",
|
||||
"trailers": "Trailers",
|
||||
"transporters": "Transportistas",
|
||||
"part_numbers": "Partes",
|
||||
"boms": "BOMs",
|
||||
"exportacion": "Exportación (operaciones)",
|
||||
"imports": "Importación (operaciones)"
|
||||
}
|
||||
},
|
||||
"config_empty": "No hay configuraciones específicas para este módulo.",
|
||||
"modal": {
|
||||
"title_pending": "Validación de importación",
|
||||
"title_success": "Importación exitosa",
|
||||
"title_warning": "Importación con observaciones",
|
||||
"desc_pending": "Revise el análisis preliminar antes de confirmar la carga de datos.",
|
||||
"desc_done": "El proceso de importación ha finalizado.",
|
||||
"total_rows": "Total filas",
|
||||
"valid_rows": "Válidos",
|
||||
"invalid_rows": "Inválidos",
|
||||
"errors": "Errores",
|
||||
"errors_heading": "Detalle de errores (para corregir en el CSV)",
|
||||
"errors_badge": "{shown} de {total} error(es)",
|
||||
"errors_truncated": "Para consultar el resto de errores, descargue el CSV.",
|
||||
"errors_missing_detail": "Se detectaron {count} fila(s) con errores pero el detalle no está disponible. Asegúrese de que el servidor esté actualizado y vuelva a subir el archivo.",
|
||||
"scan_ok_title": "Archivo validado correctamente",
|
||||
"scan_ok_body": "Todos los registros parecen correctos y listos para importar.",
|
||||
"scan_problems_title": "Se detectaron problemas en el archivo",
|
||||
"scan_problems_body": "Corrija los datos indicados abajo en su CSV y vuelva a subir, o confirme para importar solo las filas válidas (las erróneas se omitirán).",
|
||||
"inserted": "Insertados",
|
||||
"updated": "Actualizados",
|
||||
"rejected": "Rechazados",
|
||||
"rejected_hint": "Revisa el detalle por línea en la tabla inferior.",
|
||||
"ref_gaps_title": "Brechas de referencia (FK / catálogos)",
|
||||
"ref_gaps_body": "Hay {n} brecha(s) crítica(s) de referencia. Revisa catálogos y el detalle de filas rechazadas antes de reintentar.",
|
||||
"ref_state_title": "Estado de referencias",
|
||||
"ref_state_ok": "Referencias listas para operar (sin brechas críticas reportadas).",
|
||||
"ref_state_other": "Sin brechas numéricas; revisa el mensaje del servidor si aplica.",
|
||||
"skipped_reasons_heading": "Resumen de motivos de rechazo",
|
||||
"commit_errors_heading": "Detalle de errores",
|
||||
"rows_badge": "{n} filas",
|
||||
"importing_records": "Importando registros…",
|
||||
"cancel_operation": "Cancelar",
|
||||
"processing": "Procesando…",
|
||||
"confirm_load": "Confirmar carga",
|
||||
"close": "Cerrar",
|
||||
"th_line": "Línea",
|
||||
"th_column": "Columna",
|
||||
"th_message": "Mensaje",
|
||||
"th_solution": "Solución",
|
||||
"th_reference": "Referencia",
|
||||
"th_reason": "Motivo",
|
||||
"download_csv": "Descargar CSV"
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@
|
||||
import { totalSkippedFromCommit } from '$lib/csv-import-commit-metrics';
|
||||
import { countCsvDataRows } from '$lib/csv-upload-row-count';
|
||||
import CsvPendingImportsSheet from '$lib/components/dashboard/csv-upload/CsvPendingImportsSheet.svelte';
|
||||
import { csvFmt, csvMsg } from '$lib/i18n/csv-msg';
|
||||
|
||||
/** Intenta extraer un objeto tipo scan desde string tipo repr de Python. */
|
||||
function parsePythonReprScan(s: string): Record<string, unknown> | null {
|
||||
@@ -240,11 +241,11 @@
|
||||
const csvProgressStepTitle = $derived.by(() => {
|
||||
switch (csvProgressPhase) {
|
||||
case 'upload':
|
||||
return 'Subiendo archivo CSV';
|
||||
return csvMsg('progress.upload');
|
||||
case 'scan':
|
||||
return 'Validando registros en el servidor';
|
||||
return csvMsg('progress.scan');
|
||||
case 'commit':
|
||||
return 'Grabando registros en base de datos';
|
||||
return csvMsg('progress.commit');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -261,17 +262,26 @@
|
||||
switch (csvProgressPhase) {
|
||||
case 'upload':
|
||||
if (csvImportRowTotal > 0) {
|
||||
return `Archivo: ~${csvImportRowTotal} fila(s) de datos — subiendo (aún no se validan registros en servidor)…`;
|
||||
return csvFmt('progress.rows_file', { n: csvImportRowTotal });
|
||||
}
|
||||
return uploadLengthComputable
|
||||
? 'Subiendo archivo…'
|
||||
: 'Subiendo archivo (tamaño desconocido en el navegador)…';
|
||||
? csvMsg('progress.upload_known')
|
||||
: csvMsg('progress.upload_unknown');
|
||||
case 'scan':
|
||||
return `Registros procesados: ${Math.min(scanProgressCurrent, denom)} / ${denom}`;
|
||||
return csvFmt('progress.rows_scan', {
|
||||
current: Math.min(scanProgressCurrent, denom),
|
||||
total: denom
|
||||
});
|
||||
case 'commit':
|
||||
return scanProgressTotal > 0
|
||||
? `Registros grabados: ${Math.min(scanProgressCurrent, denom)} / ${denom}`
|
||||
: `Grabando en base de datos… (${Math.min(scanProgressCurrent, denom)} / ${denom} según último total conocido)`;
|
||||
? csvFmt('progress.rows_commit', {
|
||||
current: Math.min(scanProgressCurrent, denom),
|
||||
total: denom
|
||||
})
|
||||
: csvFmt('progress.rows_commit_fallback', {
|
||||
current: Math.min(scanProgressCurrent, denom),
|
||||
total: denom
|
||||
});
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
@@ -306,7 +316,7 @@
|
||||
|
||||
const csvFooterAriaValueText = $derived(
|
||||
csvFooterProgressIndeterminate
|
||||
? `${csvProgressStepTitle}. ${csvProgressDetailLine || 'En proceso…'}`
|
||||
? `${csvProgressStepTitle}. ${csvProgressDetailLine || csvMsg('progress.in_progress')}`
|
||||
: csvProgressDetailLine
|
||||
? `${csvProgressStepTitle}, ${csvFooterPercentLabel}. ${csvProgressDetailLine}`
|
||||
: `${csvProgressStepTitle}, ${csvFooterPercentLabel}`
|
||||
@@ -591,7 +601,7 @@
|
||||
csvResumeOverlayHint = false;
|
||||
skipScanCompleteToastOnce = false;
|
||||
scanPhaseJobId = null;
|
||||
currentImportLabel = (config.title && String(config.title).trim()) || null;
|
||||
currentImportLabel = csvMsg(`items.${config.id}`);
|
||||
|
||||
const onCsvFileUploadProgress = (e: { loaded: number; total: number }) => {
|
||||
if (e.total > 0) {
|
||||
@@ -626,12 +636,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -646,12 +656,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -673,12 +683,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -698,12 +708,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -721,12 +731,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -744,12 +754,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -764,12 +774,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -787,12 +797,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -810,12 +820,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -834,12 +844,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -854,12 +864,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -888,12 +898,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
return;
|
||||
@@ -915,12 +925,12 @@
|
||||
currentJobId = res.data.job_id;
|
||||
beginCsvScanAfterUpload();
|
||||
} else {
|
||||
toast.error(res.error || 'Error al subir el archivo');
|
||||
toast.error(res.error || csvMsg('toast.upload_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Upload exception', e);
|
||||
toast.error('Error inesperado al subir el archivo');
|
||||
toast.error(csvMsg('toast.upload_err_generic'));
|
||||
isUploading = false;
|
||||
}
|
||||
}
|
||||
@@ -943,11 +953,9 @@
|
||||
const errMsg = res.error || '';
|
||||
if (isStaleImportJobError(res.status, errMsg)) {
|
||||
removePendingLinkedToCurrentFlow();
|
||||
toast.info(
|
||||
'Esta importación ya no está disponible (sesión expirada o trabajo eliminado). Puedes iniciar una nueva carga.'
|
||||
);
|
||||
toast.info(csvMsg('toast.stale_job'));
|
||||
} else {
|
||||
toast.error(errMsg || 'Error al consultar el estado');
|
||||
toast.error(errMsg || csvMsg('toast.poll_err'));
|
||||
}
|
||||
isUploading = false;
|
||||
clearCsvImportSession();
|
||||
@@ -978,7 +986,7 @@
|
||||
if (skipScanCompleteToastOnce) {
|
||||
skipScanCompleteToastOnce = false;
|
||||
} else {
|
||||
toast.success('Escaneo completado. Revisa los resultados.');
|
||||
toast.success(csvMsg('toast.scan_done'));
|
||||
}
|
||||
isUploading = false;
|
||||
} else if (res.data?.status === 'failed' || res.data?.status === 'FAILURE') {
|
||||
@@ -996,7 +1004,7 @@
|
||||
commitResults = parsedCommit;
|
||||
showResultModal = true;
|
||||
finalizeCommitAndClearPending();
|
||||
toast.success('Importación completada. Revisa el listado de registros.');
|
||||
toast.success(csvMsg('toast.import_done'));
|
||||
isUploading = false;
|
||||
clearCsvImportSession();
|
||||
currentJobId = null;
|
||||
@@ -1026,7 +1034,7 @@
|
||||
if (skipScanCompleteToastOnce) {
|
||||
skipScanCompleteToastOnce = false;
|
||||
} else {
|
||||
toast.success('Escaneo completado. Revisa los resultados.');
|
||||
toast.success(csvMsg('toast.scan_done'));
|
||||
}
|
||||
isUploading = false;
|
||||
} else {
|
||||
@@ -1036,30 +1044,32 @@
|
||||
(errRaw.includes('waiting_confirmation') || (errRaw.includes('total_rows') && errRaw.includes('job_id')));
|
||||
if (looksLikeScanInError) {
|
||||
removePendingLinkedToCurrentFlow();
|
||||
toast.info('El escaneo terminó. Si no ves el modal, revisa el listado de registros.');
|
||||
toast.info(csvMsg('toast.scan_alt'));
|
||||
isUploading = false;
|
||||
clearCsvImportSession();
|
||||
currentJobId = null;
|
||||
return;
|
||||
}
|
||||
let errText: string;
|
||||
const maybeDoneMsg = csvMsg('toast.import_maybe_done');
|
||||
if (typeof errRaw === 'string') {
|
||||
errText =
|
||||
errRaw.includes('finished') && errRaw.includes('inserted')
|
||||
? 'La importación pudo completarse. Revisa el listado de registros.'
|
||||
? maybeDoneMsg
|
||||
: errRaw.includes("'status'") && errRaw.includes('waiting_confirmation')
|
||||
? 'Error al obtener el resultado. Revisa el modal de resultados.'
|
||||
? csvMsg('toast.err_fetch_scan_result')
|
||||
: errRaw;
|
||||
} else if (typeof errRaw === 'object' && errRaw !== null) {
|
||||
errText = (errRaw as { message?: string })?.message || 'Error en el procesamiento. Revisa el modal o los detalles.';
|
||||
errText =
|
||||
(errRaw as { message?: string })?.message || csvMsg('toast.err_processing_fallback');
|
||||
} else {
|
||||
errText = 'Error desconocido';
|
||||
errText = csvMsg('toast.err_unknown');
|
||||
}
|
||||
// No mostrar como error si el mensaje indica éxito
|
||||
if (errText.includes('pudo completarse') || errText.includes('Revisa el listado')) {
|
||||
if (errText === maybeDoneMsg || errText.includes(maybeDoneMsg)) {
|
||||
toast.success(errText);
|
||||
} else {
|
||||
toast.error('Error en el procesamiento: ' + errText);
|
||||
toast.error(csvFmt('toast.error_processing', { msg: errText }));
|
||||
}
|
||||
isUploading = false;
|
||||
removePendingLinkedToCurrentFlow();
|
||||
@@ -1082,12 +1092,16 @@
|
||||
if (totalOk === 0) {
|
||||
toast.error(
|
||||
backendMessage ||
|
||||
`No se insertaron ni actualizaron registros. ${totalSkipped} fueron rechazados.`
|
||||
csvFmt('toast.commit_warning_none', { skipped: totalSkipped })
|
||||
);
|
||||
} else {
|
||||
toast.warning(
|
||||
backendMessage ||
|
||||
`Se aplicaron ${totalOk} registros (${inserted} insertados, ${updated} actualizados). ${totalSkipped} rechazados.`
|
||||
csvFmt('toast.commit_warning_ok', {
|
||||
inserted,
|
||||
updated,
|
||||
skipped: totalSkipped
|
||||
})
|
||||
);
|
||||
}
|
||||
finalizeCommitAndClearPending();
|
||||
@@ -1100,15 +1114,15 @@
|
||||
const totalSkipped = totalSkippedFromCommit(res.data as Record<string, unknown>);
|
||||
|
||||
if (inserted > 0 || updated > 0) {
|
||||
const parts = [];
|
||||
if (inserted > 0) parts.push(`${inserted} insertados`);
|
||||
if (updated > 0) parts.push(`${updated} actualizados`);
|
||||
toast.success(`Importación completada: ${parts.join(', ')}`);
|
||||
const parts: string[] = [];
|
||||
if (inserted > 0) parts.push(csvFmt('toast.n_inserted', { n: inserted }));
|
||||
if (updated > 0) parts.push(csvFmt('toast.n_updated', { n: updated }));
|
||||
toast.success(csvFmt('toast.success_counts', { msg: parts.join(', ') }));
|
||||
if (totalSkipped > 0) {
|
||||
toast.warning(`${totalSkipped} registros fueron rechazados u omitidos`);
|
||||
toast.warning(csvFmt('toast.warn_skipped', { n: totalSkipped }));
|
||||
}
|
||||
} else {
|
||||
toast.error('No se insertaron registros. Revisa los errores a continuación.');
|
||||
toast.error(csvMsg('toast.finished_none'));
|
||||
}
|
||||
finalizeCommitAndClearPending();
|
||||
isUploading = false;
|
||||
@@ -1136,26 +1150,26 @@
|
||||
<!-- Single scroll: content scrolls here; padding at bottom reserves space for fixed params bar -->
|
||||
<div class="flex-1 min-h-0 overflow-y-auto p-4 md:p-8 space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 class="text-lg font-semibold md:text-2xl">Importación Masiva de Datos (CSV)</h1>
|
||||
<h1 class="text-lg font-semibold md:text-2xl">{csvMsg('page_title')}</h1>
|
||||
<CsvPendingImportsSheet companyId={companyStore.activeCompany?.id} onResume={resumePendingImport} />
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Click izquierdo: cargar archivo CSV. Click derecho: descargar estructura (plantilla).
|
||||
{csvMsg('intro_help')}
|
||||
</p>
|
||||
|
||||
<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.Trigger value="catalogos">{csvMsg('tab_catalogos')}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transportes">{csvMsg('tab_transportes')}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="importacion">{csvMsg('tab_importacion')}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="exportacion">{csvMsg('tab_exportacion')}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<div class="mt-6">
|
||||
<Tabs.Content value="catalogos" class="space-y-4">
|
||||
<!-- Catálogos: backend layouts_csv (customs_brokers, clients_and_providers, parts, boms, etc.) -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium tracking-tight">Catálogos Generales</h2>
|
||||
<h2 class="text-lg font-medium tracking-tight">{csvMsg('section_catalogs')}</h2>
|
||||
</div>
|
||||
<UploadLauncherGrid
|
||||
items={catalogosConfig}
|
||||
@@ -1167,7 +1181,7 @@
|
||||
<Tabs.Content value="transportes" class="space-y-4">
|
||||
<!-- Logística: backend layouts_csv (vehicles, drivers, trailers); transportistas sin layouts_csv -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium tracking-tight">Logística y Transporte</h2>
|
||||
<h2 class="text-lg font-medium tracking-tight">{csvMsg('section_transport')}</h2>
|
||||
</div>
|
||||
<UploadLauncherGrid
|
||||
items={transportesConfig}
|
||||
@@ -1179,7 +1193,7 @@
|
||||
<Tabs.Content value="importacion" class="space-y-4">
|
||||
<!-- Operaciones de Importación: backend layouts_csv/facturas (api.imports) -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-lg font-medium tracking-tight">Operaciones de Importación</h2>
|
||||
<h2 class="text-lg font-medium tracking-tight">{csvMsg('section_import')}</h2>
|
||||
</div>
|
||||
<UploadLauncherGrid
|
||||
items={importacionConfig}
|
||||
@@ -1190,7 +1204,7 @@
|
||||
|
||||
<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>
|
||||
<h2 class="text-lg font-medium tracking-tight">{csvMsg('section_export')}</h2>
|
||||
</div>
|
||||
<UploadLauncherGrid
|
||||
items={exportacionConfig}
|
||||
@@ -1224,7 +1238,7 @@
|
||||
aria-busy="true"
|
||||
>
|
||||
{#if csvResumeOverlayHint}
|
||||
<p class="text-xs text-muted-foreground">Reanudando la importación guardada en esta pestaña…</p>
|
||||
<p class="text-xs text-muted-foreground">{csvMsg('progress.resume_hint')}</p>
|
||||
{/if}
|
||||
{#if currentImportLabel}
|
||||
<p class="text-xs font-medium text-foreground">{currentImportLabel}</p>
|
||||
@@ -1340,7 +1354,7 @@
|
||||
pollStatus();
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('Error al iniciar la importación');
|
||||
toast.error(csvMsg('toast.commit_err'));
|
||||
isUploading = false;
|
||||
}
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user