Files
plantillas-proyectos/frontend/src/lib/components/dashboard/common/error-state.svelte
Kevin_Ramirez bdd089954b
Some checks failed
Build Producción & Push a Harbor / test (push) Failing after 3s
Build Producción & Push a Harbor / build (push) Has been skipped
Aduanasoft/plantillas-proyectos/pipeline/head There was a failure building this commit
feat: plantilla base workspace SaaS
2026-07-21 13:59:00 -05:00

159 lines
7.3 KiB
Svelte

<script lang="ts">
import { ShieldAlert, AlertTriangle, RefreshCw, ArrowLeft, Home } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import { Badge } from '$lib/components/ui/badge';
import { fade, fly } from 'svelte/transition';
import { companyStore } from '$lib/stores/company.svelte';
let {
status = 0,
error = '',
onRetry = () => { if (typeof window !== 'undefined') window.location.reload(); },
onBack = () => { if (typeof window !== 'undefined') window.history.back(); }
}: {
status?: number,
error?: string,
onRetry?: () => void,
onBack?: () => void
} = $props();
// Determinar si es un error de permisos (403)
let isForbidden = $derived(status === 403 || error.toLowerCase().includes('permission') || error.toLowerCase().includes('acceso denegado') || error.includes('403'));
// Determinar si es un error de servidor (500)
let isServerError = $derived(status >= 500 || (error && (error.toLowerCase().includes('server error') || error.toLowerCase().includes('error interno'))));
// Extraer el código del permiso si viene en el error
let permissionCode = $derived.by(() => {
if (!isForbidden) return null;
// Buscar patrones como "cat_example.view" o "Missing required permissions: cat_example.view"
const missing = error.match(/Missing required permissions?:\s*([a-z0-9_.]+)/i);
if (missing) return missing[1];
const legacy = error.match(/([a-z0-9_.]+\.[a-z0-9_.]+)/i);
if (legacy) return legacy[0];
const simple = error.match(/(cat_|settings_)[a-z0-9_.]+/i);
return simple ? simple[0] : null;
});
let title = $derived(isForbidden ? 'Acceso Restringido' : isServerError ? 'Error del Servidor' : 'Algo salió mal');
let displayError = $derived.by(() => {
if (isForbidden) {
return 'No tienes los permisos necesarios para acceder a esta sección de la plataforma.';
}
if (isServerError) {
return 'Estamos experimentando dificultades técnicas en nuestros servidores.';
}
return error || 'Ocurrió un error inesperado al intentar procesar tu solicitud.';
});
const activeCompany = $derived(companyStore.activeCompany);
</script>
<div
class="flex items-center justify-center p-6 min-h-[450px] w-full"
in:fade={{ duration: 400 }}
>
<Card.Root class="max-w-md w-full border-2 bg-card/60 backdrop-blur-md shadow-2xl overflow-hidden {isForbidden ? 'border-dashed' : 'border-destructive/20'}">
<!-- Barra de progreso decorativa -->
<div class="h-1.5 w-full bg-gradient-to-r {isForbidden ? 'from-primary/80 via-primary/40 to-primary/80' : 'from-destructive/80 via-destructive/40 to-destructive/80'} animate-gradient-x"></div>
<Card.Header class="flex flex-col items-center gap-5 pt-10 text-center">
<div class="relative">
<div
class="relative"
in:fly={{ y: 20, duration: 600, delay: 100 }}
>
<div class="absolute -inset-6 {isForbidden ? 'bg-primary/15' : 'bg-destructive/15'} rounded-full blur-2xl animate-pulse"></div>
<div class="relative bg-background p-5 rounded-full border shadow-xl">
{#if isForbidden}
<ShieldAlert class="h-14 w-14 text-primary" />
{:else}
<AlertTriangle class="h-14 w-14 text-destructive" />
{/if}
</div>
</div>
</div>
<div class="space-y-3" in:fade={{ delay: 300 }}>
<Card.Title class="text-3xl font-black tracking-tight {isForbidden ? 'text-foreground' : 'text-destructive'}">
{title}
</Card.Title>
<Card.Description class="text-base text-muted-foreground px-6 leading-relaxed">
{displayError}
</Card.Description>
</div>
</Card.Header>
<Card.Content class="flex flex-col items-center gap-8 pb-10 pt-4">
<div
class="flex flex-col items-center gap-7 w-full"
in:fade={{ delay: 500 }}
>
{#if isForbidden && permissionCode}
<div class="flex flex-col items-center gap-2.5">
<span class="text-[10px] uppercase font-bold tracking-[0.1em] text-muted-foreground/80">Identificador de Permiso</span>
<Badge variant="outline" class="font-mono text-xs bg-muted/70 border-primary/30 text-primary px-4 py-1.5 shadow-sm">
{permissionCode}
</Badge>
</div>
{/if}
{#if isServerError && error && !isForbidden}
<div class="w-full px-6">
<div class="rounded-lg bg-destructive/5 border border-destructive/10 p-4">
<p class="text-[11px] font-mono text-destructive/70 break-all text-center leading-tight">
{error.length > 150 ? error.substring(0, 150) + '...' : error}
</p>
</div>
</div>
{/if}
<div class="flex flex-col sm:flex-row items-center justify-center gap-3 w-full px-6">
{#if isServerError}
<Button variant="default" class="w-full sm:w-auto min-w-[140px] gap-2 shadow-lg hover:scale-105 transition-transform" onclick={onRetry}>
<RefreshCw class="h-4 w-4" />
Reintentar
</Button>
{/if}
<Button variant="outline" class="w-full sm:w-auto min-w-[140px] gap-2 shadow-sm" onclick={onBack}>
<ArrowLeft class="h-4 w-4" />
Regresar
</Button>
</div>
<a href="/dashboard" class="text-xs text-muted-foreground hover:text-primary transition-colors flex items-center gap-1.5">
<Home class="h-3 w-3" />
Ir al Inicio del Dashboard
</a>
</div>
</Card.Content>
<Card.Footer class="bg-muted/40 border-t py-5 justify-center">
<div class="flex flex-col items-center gap-1">
<p class="text-[10px] text-muted-foreground/80 text-center max-w-[280px]">
Si consideras que esto es un error o el problema persiste, contacta al soporte técnico.
</p>
{#if activeCompany}
<p class="text-[9px] text-muted-foreground/40 font-mono">
CID: {activeCompany.id} | TS: {new Date().toISOString()}
</p>
{/if}
</div>
</Card.Footer>
</Card.Root>
</div>
<style>
@keyframes gradient-x {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.animate-gradient-x {
background-size: 200% 200%;
animation: gradient-x 5s ease infinite;
}
</style>