159 lines
7.3 KiB
Svelte
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>
|