216 lines
6.9 KiB
Svelte
216 lines
6.9 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { browser } from '$app/environment';
|
|
import * as Sheet from '$lib/components/ui/sheet/index.js';
|
|
import { Button } from '$lib/components/ui/button/index.js';
|
|
import {
|
|
CSV_IMPORT_PENDING_CHANGED,
|
|
CSV_IMPORT_PENDING_KEY,
|
|
type CsvImportPendingEntry,
|
|
countCsvImportPendingForCompany,
|
|
listCsvImportPendingForCompany,
|
|
removeCsvImportPending,
|
|
updateCsvImportPendingSnapshot
|
|
} from '$lib/csv-import-pending';
|
|
import { fetchCsvImportStatus, isWaitingConfirmationPayload } from '$lib/csv-import-status-api';
|
|
import { Loader2, RefreshCw } from 'lucide-svelte';
|
|
|
|
type ValidatedRow = CsvImportPendingEntry & { checking?: boolean };
|
|
|
|
let {
|
|
open = $bindable(false),
|
|
companyId,
|
|
onResume
|
|
}: {
|
|
open?: boolean;
|
|
companyId: number | undefined;
|
|
onResume: (entry: CsvImportPendingEntry, scanPayload: Record<string, unknown>) => void;
|
|
} = $props();
|
|
|
|
let rows = $state<ValidatedRow[]>([]);
|
|
let refreshing = $state(false);
|
|
let badgeCount = $state(0);
|
|
|
|
function syncBadge() {
|
|
badgeCount = companyId !== undefined ? countCsvImportPendingForCompany(companyId) : 0;
|
|
}
|
|
|
|
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',
|
|
american_fractions: 'Fracciones arancelarias US',
|
|
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;
|
|
}
|
|
|
|
function isStaleJob(status: number, err: string | undefined): boolean {
|
|
if (status === 404) return true;
|
|
const m = (err || '').toLowerCase();
|
|
return /not found|no encontrado|404|expir|no disponible|invalid|inexistente/i.test(m);
|
|
}
|
|
|
|
async function validateAndLoad() {
|
|
if (companyId === undefined) {
|
|
rows = [];
|
|
return;
|
|
}
|
|
refreshing = true;
|
|
const raw = listCsvImportPendingForCompany(companyId);
|
|
const next: ValidatedRow[] = [];
|
|
for (const entry of raw) {
|
|
const res = await fetchCsvImportStatus(entry.jobId, entry.profile);
|
|
if (res.error && !res.data) {
|
|
if (isStaleJob(res.status, res.error)) {
|
|
removeCsvImportPending(entry.jobId);
|
|
continue;
|
|
}
|
|
next.push({ ...entry, checking: false });
|
|
continue;
|
|
}
|
|
if (res.data && isWaitingConfirmationPayload(res.data)) {
|
|
const d = res.data as Record<string, unknown>;
|
|
const tr = typeof d.total_rows === 'number' ? d.total_rows : undefined;
|
|
const vr = typeof d.valid_rows === 'number' ? d.valid_rows : undefined;
|
|
if (tr !== undefined || vr !== undefined) {
|
|
updateCsvImportPendingSnapshot(entry.jobId, { totalRows: tr, validRows: vr });
|
|
}
|
|
const updated = { ...entry, totalRows: tr ?? entry.totalRows, validRows: vr ?? entry.validRows };
|
|
next.push(updated);
|
|
} else {
|
|
removeCsvImportPending(entry.jobId);
|
|
}
|
|
}
|
|
rows = next;
|
|
refreshing = false;
|
|
syncBadge();
|
|
}
|
|
|
|
$effect(() => {
|
|
companyId;
|
|
syncBadge();
|
|
});
|
|
|
|
$effect(() => {
|
|
if (!browser || !open || companyId === undefined) return;
|
|
void validateAndLoad();
|
|
});
|
|
|
|
onMount(() => {
|
|
if (!browser) return;
|
|
const onPending = () => {
|
|
syncBadge();
|
|
if (open && companyId !== undefined) void validateAndLoad();
|
|
};
|
|
const onStorage = (e: StorageEvent) => {
|
|
if (e.key === CSV_IMPORT_PENDING_KEY) {
|
|
syncBadge();
|
|
if (open && companyId !== undefined) void validateAndLoad();
|
|
}
|
|
};
|
|
window.addEventListener(CSV_IMPORT_PENDING_CHANGED, onPending);
|
|
window.addEventListener('storage', onStorage);
|
|
return () => {
|
|
window.removeEventListener(CSV_IMPORT_PENDING_CHANGED, onPending);
|
|
window.removeEventListener('storage', onStorage);
|
|
};
|
|
});
|
|
|
|
function removeLocal(jobId: string) {
|
|
removeCsvImportPending(jobId);
|
|
rows = rows.filter((r) => r.jobId !== jobId);
|
|
syncBadge();
|
|
}
|
|
</script>
|
|
|
|
<Button variant="outline" size="sm" class="shrink-0" type="button" onclick={() => (open = true)}>
|
|
Pendientes
|
|
{#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}
|
|
</span>
|
|
{/if}
|
|
</Button>
|
|
|
|
<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.Description>
|
|
Escaneos listos para insertar en base de datos. Si el trabajo ya expiró en el servidor, desaparecerán al
|
|
actualizar.
|
|
</Sheet.Description>
|
|
</Sheet.Header>
|
|
<div class="flex items-center justify-end gap-2 border-b px-4 py-2">
|
|
<Button variant="outline" size="sm" disabled={refreshing} onclick={() => void validateAndLoad()}>
|
|
{#if refreshing}
|
|
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
|
{:else}
|
|
<RefreshCw class="mr-2 h-4 w-4" />
|
|
{/if}
|
|
Actualizar
|
|
</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>
|
|
{:else if rows.length === 0 && refreshing}
|
|
<p class="text-sm text-muted-foreground">Comprobando con el servidor…</p>
|
|
{:else}
|
|
<ul class="space-y-3">
|
|
{#each rows as row (row.jobId)}
|
|
<li class="rounded-lg border border-border bg-card p-3 text-sm shadow-sm">
|
|
<div class="font-medium text-foreground">
|
|
{row.label || profileLabel(row.profile)}
|
|
</div>
|
|
<div class="mt-0.5 text-xs text-muted-foreground">{profileLabel(row.profile)}</div>
|
|
{#if row.totalRows != null || row.validRows != null}
|
|
<div class="mt-1 text-xs text-muted-foreground">
|
|
{#if row.totalRows != null}
|
|
Total filas: {row.totalRows}
|
|
{/if}
|
|
{#if row.validRows != null}
|
|
<span class={row.totalRows != null ? ' · ' : ''}>Válidas: {row.validRows}</span>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
<div class="mt-1 font-mono text-[10px] text-muted-foreground/80">{row.jobId}</div>
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
<Button
|
|
size="sm"
|
|
disabled={refreshing}
|
|
onclick={async () => {
|
|
const res = await fetchCsvImportStatus(row.jobId, row.profile);
|
|
if (res.data && isWaitingConfirmationPayload(res.data)) {
|
|
onResume(row, res.data as Record<string, unknown>);
|
|
open = false;
|
|
} else {
|
|
removeCsvImportPending(row.jobId);
|
|
rows = rows.filter((r) => r.jobId !== row.jobId);
|
|
syncBadge();
|
|
}
|
|
}}
|
|
>
|
|
Reanudar
|
|
</Button>
|
|
<Button size="sm" variant="ghost" onclick={() => removeLocal(row.jobId)}>Quitar</Button>
|
|
</div>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
</div>
|
|
</Sheet.Content>
|
|
</Sheet.Root>
|