From 9548ead4d933f34e94269514f377075fe1671cec Mon Sep 17 00:00:00 2001 From: hreyes Date: Thu, 23 Apr 2026 15:46:13 -0600 Subject: [PATCH] feature/ventana-de-actualizacion-desactualizacion --- .../invoices/pdf-progress-dialog.svelte | 248 ++++++++++++++---- .../routes/dashboard/invoices/+page.svelte | 65 +++-- 2 files changed, 240 insertions(+), 73 deletions(-) diff --git a/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte index 76485457..09eb6b3a 100644 --- a/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte +++ b/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte @@ -2,9 +2,9 @@ import * as Dialog from '$lib/components/ui/dialog'; import { Progress } from '$lib/components/ui/progress'; import { invoicesReportsApi } from '$lib/api/dashboard/a76/reports/reports-invoices'; - import { toast } from 'svelte-sonner'; import { Loader2, CheckCircle2, XCircle, FileDown } from 'lucide-svelte'; import { Button } from '$lib/components/ui/button'; + import { friendlyApiErrorParts, humanizeLineReferences, type ApiResponse } from '$lib/api'; export let open = false; export let taskId: string | null = null; @@ -20,24 +20,42 @@ export let getStatus: ((taskId: string) => Promise) | null = null; + type DialogStatus = 'idle' | 'running' | 'success' | 'error' | 'validation_error'; + let progress = 0; let statusMessage = 'Iniciando...'; let pollingInterval: any = null; - let isComplete = false; - let hasError = false; + let dialogStatus: DialogStatus = 'idle'; let lastResult: any = null; let externalBody: any = null; let externalErrors: any[] | null = null; + let errorTitle = 'Ocurrió un error'; + let errorDescription = 'No se pudo completar la acción.'; + let errorList: string[] = []; + + $: isRunning = dialogStatus === 'running'; + $: isSuccess = dialogStatus === 'success'; + $: isError = dialogStatus === 'error' || dialogStatus === 'validation_error'; + $: completedStepIndex = + steps.length === 0 + ? -1 + : Math.max( + steps.findLastIndex((step) => progress >= step.percent), + isSuccess ? steps.length - 1 : -1 + ); + $: activeStepIndex = + steps.length === 0 + ? -1 + : Math.min( + steps.findIndex((step) => progress < step.percent) === -1 + ? steps.length - 1 + : steps.findIndex((step) => progress < step.percent), + steps.length - 1 + ); // Reiniciar estado cuando se abre el diálogo con un nuevo taskId $: if (open && taskId) { - progress = 0; - statusMessage = 'Iniciando...'; - isComplete = false; - hasError = false; - lastResult = null; - externalBody = null; - externalErrors = null; + resetState(); startPolling(); } else if (!open) { stopPolling(); @@ -54,16 +72,101 @@ } } + function resetState() { + progress = 0; + statusMessage = 'Iniciando...'; + dialogStatus = 'running'; + lastResult = null; + externalBody = null; + externalErrors = null; + errorTitle = 'Ocurrió un error'; + errorDescription = 'No se pudo completar la acción.'; + errorList = []; + } + + function normalizeErrorText(value: unknown, fallback: string) { + if (typeof value !== 'string') return fallback; + const normalized = humanizeLineReferences(value.trim()); + return normalized || fallback; + } + + function buildErrorPresentation( + source?: Partial | null, + result?: any + ): { status: DialogStatus; title: string; description: string; list: string[] } { + const status: DialogStatus = + result?.status === 'validation_error' ? 'validation_error' : 'error'; + const validationErrors = source?.validationErrors || result?.validationErrors; + + if (validationErrors?.length) { + const { title, description } = friendlyApiErrorParts({ + status: source?.status ?? 400, + error: source?.error || result?.message || 'Error de validación', + validationErrors + }); + return { + status, + title, + description, + list: [] as string[] + }; + } + + const resultErrors = Array.isArray(result?.errors) + ? result.errors + .map((entry: any) => normalizeErrorText(entry?.message || String(entry), 'Error de validación')) + .filter(Boolean) + : []; + + if (resultErrors.length > 0) { + return { + status, + title: + status === 'validation_error' + ? 'Se encontraron errores de validación' + : 'No se pudo completar la acción', + description: + normalizeErrorText(result?.message, resultErrors[0] || 'Revisa los datos e intenta de nuevo.'), + list: resultErrors + }; + } + + const fallbackDescription = normalizeErrorText( + source?.error || result?.message, + status === 'validation_error' + ? 'Se encontraron errores de validación. Revisa los datos e intenta de nuevo.' + : 'No se pudo completar la acción. Intenta de nuevo.' + ); + + return { + status, + title: + status === 'validation_error' + ? 'Se encontraron errores de validación' + : 'No se pudo completar la acción', + description: fallbackDescription, + list: [] as string[] + }; + } + + function setErrorState(source?: Partial | null, result?: any) { + const presentation = buildErrorPresentation(source, result); + lastResult = result ?? lastResult; + dialogStatus = presentation.status; + errorTitle = presentation.title; + errorDescription = presentation.description; + errorList = presentation.list; + statusMessage = presentation.description; + stopPolling(); + } + async function pollOnce() { if (!taskId) return; const apiCall = getStatus || invoicesReportsApi.getTaskStatus; const raw = await apiCall(taskId); const response = raw?.data !== undefined ? raw.data : raw; if (raw?.error) { - hasError = true; - statusMessage = `Error: ${raw.error}`; - stopPolling(); - toast.error(raw.error); + setErrorState(raw, response); return; } if (response?.state === 'PROCESSING' && response.info) { @@ -71,39 +174,48 @@ statusMessage = response.info.status || 'Procesando...'; } else if (response?.state === 'SUCCESS') { const result = response.result; - stopPolling(); lastResult = result; if (result?.status === 'validation_error' || result?.status === 'error') { - // Quedarse abierto, marcar error y dejar que onComplete maneje los mensajes - hasError = true; - isComplete = true; + setErrorState( + { + status: response?.status ?? 400, + error: result?.message, + validationErrors: result?.validationErrors + }, + result + ); onComplete(result); } else { progress = 100; statusMessage = '¡Completado!'; - isComplete = true; + dialogStatus = 'success'; + stopPolling(); onComplete(result); } } else if (response?.state === 'FAILURE') { - hasError = true; const errMsg = response.result ? String(response.result) : 'Error desconocido'; - statusMessage = `Error: ${errMsg}`; - stopPolling(); - toast.error(`Falló: ${errMsg}`); + setErrorState({ status: response?.status ?? 500, error: errMsg }, response?.result); } } async function startPolling() { stopPolling(); // Asegurar limpieza previa - await pollOnce(); // Primer poll inmediato para mostrar progreso sin esperar 1s + try { + await pollOnce(); // Primer poll inmediato para mostrar progreso sin esperar 1s + } catch (error) { + console.error('Error polling task status:', error); + setErrorState({ status: 500, error: error instanceof Error ? error.message : String(error) }); + return; + } pollingInterval = setInterval(async () => { - if (isComplete || hasError) return; + if (!isRunning) return; try { await pollOnce(); } catch (error) { console.error('Error polling task status:', error); + setErrorState({ status: 500, error: error instanceof Error ? error.message : String(error) }); } }, 1000); } @@ -128,11 +240,17 @@
{#each steps as step, i} - {@const isDone = progress >= step.percent} - {@const isCurrent = !isDone && (i === 0 || progress >= steps[i - 1]?.percent)} + {@const isDone = i < activeStepIndex || (isSuccess && progress >= step.percent)} + {@const isReached = i <= completedStepIndex} + {@const isCurrent = isRunning && i === activeStepIndex} + {@const isFailed = isError && i === activeStepIndex}
{i + 1}. - + {step.label} {#if isDone} 100% + {:else if isFailed} + {progress}% + {:else if isReached} + 100% {:else if isCurrent} {progress}% {:else} @@ -154,6 +284,10 @@ {#if isDone} + {:else if isFailed} + + {:else if isReached} + {:else if isCurrent} {/if} @@ -164,27 +298,29 @@
{#if statusMessage} -
+
{statusMessage}
{/if} {:else}
- {statusMessage} + {statusMessage} {progress}%
{/if} -
- {#if isComplete} +
+ {#if isSuccess}
{#if lastResult?.cove_number} COVE generado - {:else if lastResult?.status === 'validation_error'} - Se encontraron errores de validación {:else} {completeMessage} {/if} @@ -205,30 +341,40 @@ {/if} - - {#if lastResult?.status === 'validation_error' && externalBody} - - {externalBody.mensaje || 'Datos inválidos en el servicio COVE.'} - - {#if externalErrors && externalErrors.length} -
    - {#each externalErrors as e} -
  • {e.campo}: {e.mensaje}
  • - {/each} -
- {/if} - {:else if lastResult?.message} + {#if lastResult?.message} {lastResult.message} {/if}
- {:else if hasError} + {:else if isError}
- Ocurrió un error +
+

{errorTitle}

+

{errorDescription}

+ {#if errorList.length > 0} +
    + {#each errorList as item} +
  • {item}
  • + {/each} +
+ {/if} + {#if dialogStatus === 'validation_error' && externalBody} +

+ {externalBody.mensaje || 'Datos inválidos en el servicio COVE.'} +

+ {#if externalErrors && externalErrors.length} +
    + {#each externalErrors as e} +
  • {e.campo}: {e.mensaje}
  • + {/each} +
+ {/if} + {/if} +
{:else}
@@ -239,7 +385,7 @@
- {#if isComplete || hasError} + {#if isSuccess || isError} {/if} diff --git a/frontend/src/routes/dashboard/invoices/+page.svelte b/frontend/src/routes/dashboard/invoices/+page.svelte index 972c747b..97743e07 100644 --- a/frontend/src/routes/dashboard/invoices/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/+page.svelte @@ -60,6 +60,7 @@ import { useShortcuts } from '$lib/hooks/use-shortcuts'; import { obtenerAtajosListaFacturas } from '$lib/config/shortcuts/dashboard/invoices/list'; import { m } from '$lib/i18n/messages'; + import { humanizeLineReferences } from '$lib/api'; // Los datos iniciales vienen del servidor let { data }: { data: PageData } = $props(); @@ -452,6 +453,9 @@ let currentStatusFunction = $state<((taskId: string) => Promise) | null>(null); let progressDialogTitle = $state('Generando documento'); let progressDialogSteps = $state<{ label: string; percent: number }[] | null>(null); + let isProgressErrorOpen = $state(false); + let progressErrorTitle = $state('Ocurrió un error'); + let progressErrorDescription = $state('No se pudo completar la acción.'); let coveRecipientsLoading = $state(false); let coveRecipientsError = $state(null); let coveRecipientEmail = $state(''); @@ -820,7 +824,6 @@ } function onPdfComplete(result: any) { - // Esta función se llama cuando el diálogo reporta SUCCESS try { if (result.status === 'success') { if (result.content) { @@ -856,23 +859,9 @@ m.invoice_list_toasts_cove_external_queued_default(); const taskInfo = result.external_task_id ? ` (task_id: ${result.external_task_id})` : ''; toast.success(baseMsg + taskInfo); - } else if (result.status === 'validation_error') { - const errors: any[] = result.errors || []; - const preview = errors - .slice(0, 3) - .map((e: any) => `• ${e.message}`) - .join('\n'); - const extra = - errors.length > 3 - ? m.invoice_list_toasts_validation_extra_more({ count: String(errors.length - 3) }) - : ''; - toast.error( - m.invoice_list_toasts_validation_error_count({ - count: String(errors.length), - preview, - extra - }) - ); + } else if (result.status === 'validation_error' || result.status === 'error') { + reloadData(); + return; } else if ( typeof result.message === 'string' && result.message.includes('Factura COVE iniciada para') @@ -1138,7 +1127,8 @@ ); if (response.error) { - toast.error( + openProgressErrorDialog( + 'No se pudo iniciar el procesamiento de la factura', m.invoice_list_toasts_process_start_error_prefix({ error: String(response.error) }) ); return; @@ -1151,7 +1141,10 @@ showProgressDialog = true; } catch (e) { console.error('Error al iniciar proceso de factura:', e); - toast.error(m.invoice_list_toasts_process_start_error()); + openProgressErrorDialog( + 'No se pudo iniciar el procesamiento de la factura', + m.invoice_list_toasts_process_start_error() + ); } } @@ -1166,7 +1159,8 @@ ); if (response.error) { - toast.error( + openProgressErrorDialog( + 'No se pudo iniciar la desactualización de la factura', m.invoice_list_toasts_revert_start_error_prefix({ error: String(response.error) }) ); return; @@ -1179,7 +1173,10 @@ showProgressDialog = true; } catch (e) { console.error('Error al iniciar des-actualización de factura:', e); - toast.error(m.invoice_list_toasts_revert_start_error()); + openProgressErrorDialog( + 'No se pudo iniciar la desactualización de la factura', + m.invoice_list_toasts_revert_start_error() + ); } } @@ -1316,6 +1313,12 @@ progressDialogSteps = null; } + function openProgressErrorDialog(title: string, description: string) { + progressErrorTitle = title; + progressErrorDescription = humanizeLineReferences(description.trim()) || description; + isProgressErrorOpen = true; + } + // --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS --- const columns = createColumns(handleSuccess); async function handleModalConfirm( @@ -1590,6 +1593,24 @@ } /> + + + + {progressErrorTitle} + +
+ {progressErrorDescription} +
+
+
+ + (isProgressErrorOpen = false)}> + Cerrar + + +
+
+