313 lines
10 KiB
Svelte
313 lines
10 KiB
Svelte
<script lang="ts">
|
|
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
|
import { Button } from '$lib/components/ui/button/index.js';
|
|
import {
|
|
Loader2,
|
|
CheckCircle2,
|
|
AlertTriangle,
|
|
XCircle,
|
|
FileText,
|
|
UploadCloud
|
|
} from 'lucide-svelte';
|
|
|
|
let {
|
|
open = $bindable(false),
|
|
scanResults = null,
|
|
commitResults = null,
|
|
isUploading = false,
|
|
onConfirm,
|
|
onCancel,
|
|
onClose
|
|
}: {
|
|
open: boolean;
|
|
scanResults: any;
|
|
commitResults: any;
|
|
isUploading: boolean;
|
|
onConfirm: () => void;
|
|
onCancel: () => void;
|
|
onClose: () => void;
|
|
} = $props();
|
|
|
|
// Determine state
|
|
let isPending = $derived(!!scanResults && !commitResults);
|
|
let isFinished = $derived(!!commitResults);
|
|
|
|
// Derived metrics for UI logic
|
|
let hasErrors = $derived(
|
|
scanResults?.error_count > 0 ||
|
|
commitResults?.skipped_invalid > 0 ||
|
|
commitResults?.skipped_missing_fk > 0
|
|
);
|
|
|
|
let totalSkipped = $derived(
|
|
(commitResults?.skipped_invalid || 0) +
|
|
(commitResults?.skipped_missing_fk || 0) +
|
|
(commitResults?.skipped_missing_invoice || 0) +
|
|
(commitResults?.skipped_duplicate || 0)
|
|
);
|
|
|
|
function handleOpenChange(newOpen: boolean) {
|
|
if (!newOpen) {
|
|
if (isPending) onCancel();
|
|
else onClose();
|
|
}
|
|
open = newOpen;
|
|
}
|
|
</script>
|
|
|
|
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
|
|
<Dialog.Content
|
|
class="sm:max-w-[650px] p-0 gap-0 overflow-hidden border-0 shadow-xl flex flex-col max-h-[90vh] bg-background text-foreground"
|
|
>
|
|
<!-- Header Section with Theme Colors -->
|
|
<div
|
|
class="px-6 py-5 border-b flex items-start gap-4
|
|
{isPending
|
|
? 'bg-primary/5'
|
|
: isFinished && !hasErrors
|
|
? 'bg-green-50/50 dark:bg-green-900/10'
|
|
: 'bg-destructive/5'}"
|
|
>
|
|
<div
|
|
class="p-2 rounded-full ring-1 ring-inset
|
|
{isPending
|
|
? 'bg-primary/10 text-primary ring-primary/20'
|
|
: isFinished && !hasErrors
|
|
? 'bg-green-100 text-green-700 ring-green-200 dark:bg-green-900/20 dark:text-green-400 dark:ring-green-900/40'
|
|
: 'bg-destructive/10 text-destructive ring-destructive/20'}"
|
|
>
|
|
{#if isPending}
|
|
<FileText class="w-6 h-6" />
|
|
{:else if isFinished && !hasErrors}
|
|
<CheckCircle2 class="w-6 h-6" />
|
|
{:else}
|
|
<AlertTriangle class="w-6 h-6" />
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex-1">
|
|
<Dialog.Title class="text-xl font-semibold tracking-tight text-foreground">
|
|
{#if isPending}
|
|
Validación de Importación
|
|
{:else if isFinished}
|
|
{hasErrors ? 'Importación con Observaciones' : 'Importación Exitosa'}
|
|
{/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.
|
|
{:else if isFinished}
|
|
El proceso de importación ha finalizado.
|
|
{/if}
|
|
</Dialog.Description>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Scrollable Content -->
|
|
<div class="px-6 py-6 overflow-y-auto">
|
|
<!-- PENDING STATE CONTENT -->
|
|
{#if isPending}
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
|
<!-- Card: Total -->
|
|
<div
|
|
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
|
|
>
|
|
<span class="text-2xl font-bold text-foreground">{scanResults.total_rows || 0}</span>
|
|
</div>
|
|
|
|
<!-- Card: Valid -->
|
|
<div
|
|
class="bg-green-50/50 dark:bg-green-900/10 p-4 rounded-lg border border-green-100 dark:border-green-900/30 flex flex-col items-center justify-center text-center shadow-sm"
|
|
>
|
|
<span
|
|
class="text-green-600 dark:text-green-400 text-xs uppercase font-bold tracking-wider mb-1"
|
|
>Válidos</span
|
|
>
|
|
<span class="text-2xl font-bold text-green-700 dark:text-green-300"
|
|
>{scanResults.valid_rows || 0}</span
|
|
>
|
|
</div>
|
|
|
|
<!-- Card: Errors -->
|
|
<div
|
|
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
|
|
>
|
|
<span class="text-2xl font-bold text-destructive">{scanResults.error_count || 0}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{#if scanResults.error_count > 0}
|
|
<div
|
|
class="rounded-md bg-destructive/5 border border-destructive/10 p-4 flex items-start gap-3 mb-4"
|
|
>
|
|
<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>
|
|
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).
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{#if scanResults.errors && scanResults.errors.length > 0}
|
|
<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)
|
|
</h5>
|
|
<span
|
|
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
|
|
>
|
|
{scanResults.errors.length} error(es)
|
|
</span>
|
|
</div>
|
|
<div class="max-h-60 overflow-y-auto bg-card relative">
|
|
<table class="w-full text-xs text-left">
|
|
<thead
|
|
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>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y">
|
|
{#each scanResults.errors as err}
|
|
<tr class="hover:bg-muted/30 transition-colors">
|
|
<td class="px-4 py-2 font-mono text-muted-foreground">{err.line}</td>
|
|
<td class="px-4 py-2 font-mono font-medium text-foreground">{err.col || '-'}</td>
|
|
<td class="px-4 py-2 text-destructive">{err.msg || '-'}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{/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>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
<!-- FINISHED STATE CONTENT -->
|
|
{#if isFinished}
|
|
<div class="space-y-6">
|
|
<!-- Simplified 2-Column Stats Grid -->
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<!-- Inserted (Green) -->
|
|
<div
|
|
class="bg-green-50/50 dark:bg-green-900/10 p-4 rounded-lg border border-green-100 dark:border-green-900/30 flex flex-col items-center justify-center text-center shadow-sm"
|
|
>
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<CheckCircle2 class="w-4 h-4 text-green-600 dark:text-green-400" />
|
|
<span
|
|
class="text-green-600 dark:text-green-400 text-xs uppercase font-bold tracking-wider"
|
|
>Insertados</span
|
|
>
|
|
</div>
|
|
<span class="text-3xl font-bold text-green-700 dark:text-green-300"
|
|
>{commitResults.inserted || 0}</span
|
|
>
|
|
</div>
|
|
|
|
<!-- Rejected (Red) -->
|
|
<div
|
|
class="bg-destructive/5 p-4 rounded-lg border border-destructive/10 flex flex-col items-center justify-center text-center shadow-sm"
|
|
>
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<XCircle class="w-4 h-4 text-destructive" />
|
|
<span class="text-destructive text-xs uppercase font-bold tracking-wider"
|
|
>Rechazados</span
|
|
>
|
|
</div>
|
|
<span class="text-3xl font-bold text-destructive">{totalSkipped}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Details Table -->
|
|
{#if commitResults.skipped_details && commitResults.skipped_details.length > 0}
|
|
<div 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
|
|
</h5>
|
|
<span
|
|
class="text-[10px] bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border"
|
|
>
|
|
{commitResults.skipped_details.length} filas
|
|
</span>
|
|
</div>
|
|
<div class="max-h-60 overflow-y-auto bg-card relative">
|
|
<table class="w-full text-xs text-left">
|
|
<thead
|
|
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>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y">
|
|
{#each commitResults.skipped_details as detail}
|
|
<tr class="hover:bg-muted/30 transition-colors">
|
|
<td class="px-4 py-2 font-mono text-muted-foreground">{detail.line}</td>
|
|
<td class="px-4 py-2 font-mono font-medium text-foreground"
|
|
>{detail.invoice || '-'}</td
|
|
>
|
|
<td class="px-4 py-2 text-destructive">{detail.reason}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Footer Actions -->
|
|
<div class="px-6 py-4 bg-muted/20 border-t flex items-center justify-end gap-3">
|
|
{#if isPending}
|
|
<Button
|
|
variant="ghost"
|
|
onclick={onCancel}
|
|
disabled={isUploading}
|
|
class="text-muted-foreground hover:bg-muted/50"
|
|
>
|
|
Cancelar Operación
|
|
</Button>
|
|
<Button
|
|
onclick={onConfirm}
|
|
disabled={isUploading}
|
|
class="bg-primary hover:bg-primary/90 text-primary-foreground min-w-[140px] shadow-sm"
|
|
>
|
|
{#if isUploading}
|
|
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
|
Procesando...
|
|
{:else}
|
|
<UploadCloud class="mr-2 h-4 w-4" />
|
|
Confirmar Carga
|
|
{/if}
|
|
</Button>
|
|
{:else if isFinished}
|
|
<Button variant="outline" onclick={onClose} class="min-w-[100px]">Cerrar</Button>
|
|
{/if}
|
|
</div>
|
|
</Dialog.Content>
|
|
</Dialog.Root>
|