|
|
|
|
@@ -5,17 +5,16 @@
|
|
|
|
|
Field,
|
|
|
|
|
FieldLabel,
|
|
|
|
|
FieldDescription,
|
|
|
|
|
FieldSeparator,
|
|
|
|
|
} from "$lib/components/ui/field/index.js";
|
|
|
|
|
import { Input } from "$lib/components/ui/input/index.js";
|
|
|
|
|
import { Button } from "$lib/components/ui/button/index.js";
|
|
|
|
|
import { cn } from "$lib/utils.js";
|
|
|
|
|
import { FileText, ShieldCheck } from 'lucide-svelte';
|
|
|
|
|
import faviconUrl from '$lib/assets/favicon.svg';
|
|
|
|
|
import type { HTMLAttributes } from "svelte/elements";
|
|
|
|
|
import { page } from '$app/state';
|
|
|
|
|
import { enhance } from '$app/forms';
|
|
|
|
|
import { loginWithProvider } from '$lib/sso';
|
|
|
|
|
import { onMount } from 'svelte';
|
|
|
|
|
import { onMount, tick } from 'svelte';
|
|
|
|
|
|
|
|
|
|
let { class: className, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
|
|
|
|
|
|
|
|
|
|
@@ -23,14 +22,20 @@
|
|
|
|
|
|
|
|
|
|
let username = $state('demo');
|
|
|
|
|
let password = $state('demo123');
|
|
|
|
|
let tenantSlug = $state('aduanasoft');
|
|
|
|
|
let tenantSlug = $state('');
|
|
|
|
|
let loading = $state(false);
|
|
|
|
|
|
|
|
|
|
// Obtener el error del servidor si existe
|
|
|
|
|
// step 1 = credenciales, step 2 = selección de organización
|
|
|
|
|
let step = $state<1 | 2>(1);
|
|
|
|
|
let readyToSubmit = $state(false);
|
|
|
|
|
let formEl: HTMLFormElement | undefined = $state();
|
|
|
|
|
|
|
|
|
|
// Descubrimiento de tenants
|
|
|
|
|
type TenantInfo = { id: number; name: string; slug: string };
|
|
|
|
|
let tenants = $state<TenantInfo[]>([]);
|
|
|
|
|
|
|
|
|
|
const error = $derived(page.form?.error || '');
|
|
|
|
|
|
|
|
|
|
// Limpiar todo el localStorage y cookies al montar el componente de login
|
|
|
|
|
// Esto asegura que no queden datos del tenant anterior
|
|
|
|
|
onMount(() => {
|
|
|
|
|
clearAllData();
|
|
|
|
|
});
|
|
|
|
|
@@ -46,59 +51,100 @@
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Función para limpiar todo el localStorage y cookies
|
|
|
|
|
function clearAllData() {
|
|
|
|
|
if (typeof localStorage !== 'undefined') {
|
|
|
|
|
localStorage.removeItem('access_token');
|
|
|
|
|
localStorage.removeItem('refresh_token');
|
|
|
|
|
localStorage.removeItem('activeCompanyId');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearClientCookies();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchTenants(): Promise<TenantInfo[]> {
|
|
|
|
|
try {
|
|
|
|
|
const apiBase = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, '');
|
|
|
|
|
// Llama a /login SIN tenant_slug: el backend verifica credenciales primero,
|
|
|
|
|
// luego devuelve las orgs. Sin contraseña válida no se revela nada.
|
|
|
|
|
const res = await fetch(`${apiBase}/v1/auth/login`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ username, password }),
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
// { status: "choose_tenant", tenants: [...] }
|
|
|
|
|
if (data.tenants) return data.tenants;
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore — el server action mostrará el error de autenticación
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Llama al backend real desde el paso 2
|
|
|
|
|
function confirmTenant() {
|
|
|
|
|
if (!tenantSlug) return;
|
|
|
|
|
readyToSubmit = true;
|
|
|
|
|
formEl?.requestSubmit();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function goBack() {
|
|
|
|
|
step = 1;
|
|
|
|
|
tenantSlug = '';
|
|
|
|
|
tenants = [];
|
|
|
|
|
readyToSubmit = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleMicrosoftLogin() {
|
|
|
|
|
// Limpiar datos antes de iniciar SSO
|
|
|
|
|
clearAllData();
|
|
|
|
|
|
|
|
|
|
// Guardar el tenant_slug en localStorage para recuperarlo después del callback
|
|
|
|
|
if (tenantSlug) {
|
|
|
|
|
localStorage.setItem('pending_tenant_slug', tenantSlug);
|
|
|
|
|
}
|
|
|
|
|
if (tenantSlug) localStorage.setItem('pending_tenant_slug', tenantSlug);
|
|
|
|
|
loginWithProvider('microsoft');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleGoogleLogin() {
|
|
|
|
|
// Limpiar datos antes de iniciar SSO
|
|
|
|
|
clearAllData();
|
|
|
|
|
|
|
|
|
|
// Guardar el tenant_slug en localStorage para recuperarlo después del callback
|
|
|
|
|
if (tenantSlug) {
|
|
|
|
|
localStorage.setItem('pending_tenant_slug', tenantSlug);
|
|
|
|
|
}
|
|
|
|
|
if (tenantSlug) localStorage.setItem('pending_tenant_slug', tenantSlug);
|
|
|
|
|
loginWithProvider('google');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleAppleLogin() {
|
|
|
|
|
// Apple no está configurado por defecto en Keycloak,
|
|
|
|
|
// pero puedes agregarlo siguiendo el mismo patrón
|
|
|
|
|
alert('Apple SSO no está configurado aún');
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div class={cn("flex flex-col gap-6", className)} {...restProps}>
|
|
|
|
|
<Card.Root class="overflow-hidden p-0">
|
|
|
|
|
<Card.Root class="overflow-hidden p-0 shadow-2xl border-0">
|
|
|
|
|
<Card.Content class="grid p-0 md:grid-cols-2">
|
|
|
|
|
<!-- Formulario -->
|
|
|
|
|
<form
|
|
|
|
|
class="p-6 md:p-8"
|
|
|
|
|
bind:this={formEl}
|
|
|
|
|
class="p-8 md:p-10 flex flex-col justify-center"
|
|
|
|
|
method="POST"
|
|
|
|
|
use:enhance={() => {
|
|
|
|
|
use:enhance={async ({ cancel }) => {
|
|
|
|
|
// Interceptar solo en el paso 1 antes de hacer el submit real
|
|
|
|
|
if (!readyToSubmit) {
|
|
|
|
|
cancel();
|
|
|
|
|
loading = true;
|
|
|
|
|
tenants = await fetchTenants();
|
|
|
|
|
loading = false;
|
|
|
|
|
if (tenants.length === 1) {
|
|
|
|
|
// 1 sola org: login directo
|
|
|
|
|
tenantSlug = tenants[0].slug;
|
|
|
|
|
readyToSubmit = true;
|
|
|
|
|
await tick(); // esperar a que el DOM refleje tenantSlug antes de enviar
|
|
|
|
|
formEl?.requestSubmit();
|
|
|
|
|
} else if (tenants.length > 1) {
|
|
|
|
|
// Varias orgs: mostrar selector
|
|
|
|
|
step = 2;
|
|
|
|
|
} else {
|
|
|
|
|
// 0 orgs: enviar igual, el backend rechazará
|
|
|
|
|
readyToSubmit = true;
|
|
|
|
|
await tick();
|
|
|
|
|
formEl?.requestSubmit();
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
loading = true;
|
|
|
|
|
return async ({ update, result }) => {
|
|
|
|
|
await update();
|
|
|
|
|
loading = false;
|
|
|
|
|
|
|
|
|
|
// Si hay un error, limpiar cookies del cliente
|
|
|
|
|
readyToSubmit = false;
|
|
|
|
|
if (result.type === 'failure') {
|
|
|
|
|
clearClientCookies();
|
|
|
|
|
}
|
|
|
|
|
@@ -106,131 +152,218 @@
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<FieldGroup>
|
|
|
|
|
<div class="flex flex-col items-center gap-2 text-center">
|
|
|
|
|
<h1 class="text-2xl font-bold bg-gradient-to-r from-blue-600 to-blue-800 bg-clip-text text-transparent">Anexo 76</h1>
|
|
|
|
|
<p class="text-muted-foreground text-balance text-sm">
|
|
|
|
|
Sistema de Cumplimiento Fiscal y Aduanal
|
|
|
|
|
</p>
|
|
|
|
|
<!-- Logo / Branding -->
|
|
|
|
|
<div class="flex flex-col items-center gap-3 text-center mb-2">
|
|
|
|
|
<img src={faviconUrl} alt="Anexo 76" class="w-14 h-14 rounded-2xl shadow-lg shadow-blue-200 dark:shadow-blue-900/50" />
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="text-2xl font-bold tracking-tight text-slate-900 dark:text-slate-100">Anexo 76</h1>
|
|
|
|
|
<p class="text-muted-foreground text-sm mt-0.5">
|
|
|
|
|
Sistema de Cumplimiento Fiscal y Aduanal
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{#if error}
|
|
|
|
|
<div class="rounded-md bg-red-50 p-4 text-sm text-red-800">
|
|
|
|
|
<div class="flex items-start gap-3 rounded-xl bg-red-50 dark:bg-red-950/40 border border-red-200 dark:border-red-800 p-4 text-sm text-red-700 dark:text-red-400">
|
|
|
|
|
<svg class="w-4 h-4 mt-0.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
|
|
|
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<Field>
|
|
|
|
|
<FieldLabel for="tenant-{id}">Tenant</FieldLabel>
|
|
|
|
|
<Input
|
|
|
|
|
id="tenant-{id}"
|
|
|
|
|
name="tenant_slug"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="aduanasoft"
|
|
|
|
|
bind:value={tenantSlug}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
</Field>
|
|
|
|
|
|
|
|
|
|
<Field>
|
|
|
|
|
<FieldLabel for="username-{id}">Usuario</FieldLabel>
|
|
|
|
|
<Input
|
|
|
|
|
id="username-{id}"
|
|
|
|
|
name="username"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="demo"
|
|
|
|
|
bind:value={username}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
</Field>
|
|
|
|
|
<Field>
|
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
<FieldLabel for="password-{id}">Contraseña</FieldLabel>
|
|
|
|
|
<a href="##" class="ml-auto text-sm underline-offset-2 hover:underline">
|
|
|
|
|
¿Olvidaste tu contraseña?
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<!-- Campos ocultos siempre presentes para el submit en paso 2 -->
|
|
|
|
|
<input type="hidden" name="tenant_slug" value={tenantSlug} />
|
|
|
|
|
{#if step === 2}
|
|
|
|
|
<input type="hidden" name="username" value={username} />
|
|
|
|
|
<input type="hidden" name="password" value={password} />
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if step === 1}
|
|
|
|
|
<!-- PASO 1: Credenciales -->
|
|
|
|
|
<Field>
|
|
|
|
|
<FieldLabel for="username-{id}" class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Usuario</FieldLabel>
|
|
|
|
|
<Input
|
|
|
|
|
id="username-{id}"
|
|
|
|
|
name="username"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="usuario@empresa.com"
|
|
|
|
|
bind:value={username}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
class="h-11"
|
|
|
|
|
/>
|
|
|
|
|
</Field>
|
|
|
|
|
|
|
|
|
|
<Field>
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
<FieldLabel for="password-{id}" class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">Contraseña</FieldLabel>
|
|
|
|
|
<a href="##" class="text-xs text-blue-600 hover:text-blue-700 dark:text-blue-400 font-medium hover:underline underline-offset-2">
|
|
|
|
|
¿Olvidaste tu contraseña?
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
<Input
|
|
|
|
|
id="password-{id}"
|
|
|
|
|
name="password"
|
|
|
|
|
type="password"
|
|
|
|
|
bind:value={password}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
class="h-11"
|
|
|
|
|
/>
|
|
|
|
|
</Field>
|
|
|
|
|
|
|
|
|
|
<Field>
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={loading}
|
|
|
|
|
class="h-11 w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 font-semibold shadow-md shadow-blue-200 dark:shadow-blue-900/40 transition-all duration-200"
|
|
|
|
|
>
|
|
|
|
|
{#if loading}
|
|
|
|
|
<svg class="animate-spin w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
Verificando...
|
|
|
|
|
{:else}
|
|
|
|
|
Iniciar sesión
|
|
|
|
|
{/if}
|
|
|
|
|
</Button>
|
|
|
|
|
</Field>
|
|
|
|
|
|
|
|
|
|
<div class="relative flex items-center gap-3 my-1">
|
|
|
|
|
<div class="flex-1 h-px bg-border"></div>
|
|
|
|
|
<span class="text-xs text-muted-foreground font-medium px-1">o continúa con</span>
|
|
|
|
|
<div class="flex-1 h-px bg-border"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<Input
|
|
|
|
|
id="password-{id}"
|
|
|
|
|
name="password"
|
|
|
|
|
type="password"
|
|
|
|
|
bind:value={password}
|
|
|
|
|
required
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
</Field>
|
|
|
|
|
<Field>
|
|
|
|
|
<Button type="submit" disabled={loading}>
|
|
|
|
|
{loading ? 'Iniciando sesión...' : 'Iniciar sesión'}
|
|
|
|
|
</Button>
|
|
|
|
|
</Field>
|
|
|
|
|
<FieldSeparator class="*:data-[slot=field-separator-content]:bg-card">
|
|
|
|
|
O continua con
|
|
|
|
|
</FieldSeparator>
|
|
|
|
|
<Field class="grid grid-cols-3 gap-4">
|
|
|
|
|
<Button variant="outline" type="button" onclick={handleAppleLogin} disabled={loading}>
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
|
|
|
<path
|
|
|
|
|
d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Field class="grid grid-cols-2 gap-3">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={handleGoogleLogin}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
class="h-11 border hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" fill="currentColor"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="sr-only">Google</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={handleMicrosoftLogin}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
class="h-11 border hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M11.4 24H0V12.6h11.4V24zM24 24H12.6V12.6H24V24zM11.4 11.4H0V0h11.4v11.4zm12.6 0H12.6V0H24v11.4z" fill="currentColor"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="sr-only">Microsoft</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</Field>
|
|
|
|
|
|
|
|
|
|
<p class="text-center text-sm text-muted-foreground">
|
|
|
|
|
¿No tienes cuenta?{' '}
|
|
|
|
|
<a href="/register" class="font-semibold text-blue-600 hover:text-blue-700 dark:text-blue-400 hover:underline underline-offset-2">
|
|
|
|
|
Regístrate
|
|
|
|
|
</a>
|
|
|
|
|
</p>
|
|
|
|
|
{:else}
|
|
|
|
|
<!-- PASO 2: Selección de organización -->
|
|
|
|
|
<div class="flex flex-col gap-1 text-center">
|
|
|
|
|
<p class="text-sm font-medium text-slate-700 dark:text-slate-300">Selecciona tu organización</p>
|
|
|
|
|
<p class="text-xs text-muted-foreground">Tu cuenta tiene acceso a varias organizaciones</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="flex flex-col gap-2">
|
|
|
|
|
{#each tenants as t}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => (tenantSlug = t.slug)}
|
|
|
|
|
class="flex items-center gap-3 rounded-xl border-2 px-4 py-3 text-left transition-all duration-150 hover:border-blue-400 hover:bg-blue-50 dark:hover:bg-blue-950/40
|
|
|
|
|
{tenantSlug === t.slug
|
|
|
|
|
? 'border-blue-500 bg-blue-50 dark:bg-blue-950/40'
|
|
|
|
|
: 'border-input bg-background'}"
|
|
|
|
|
>
|
|
|
|
|
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 font-bold text-sm">
|
|
|
|
|
{t.name.charAt(0).toUpperCase()}
|
|
|
|
|
</span>
|
|
|
|
|
<span class="flex-1 text-sm font-medium text-slate-800 dark:text-slate-200">{t.name}</span>
|
|
|
|
|
{#if tenantSlug === t.slug}
|
|
|
|
|
<svg class="w-4 h-4 text-blue-500 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/>
|
|
|
|
|
</svg>
|
|
|
|
|
{/if}
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Field>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={confirmTenant}
|
|
|
|
|
disabled={!tenantSlug || loading}
|
|
|
|
|
class="h-11 w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 font-semibold shadow-md shadow-blue-200 dark:shadow-blue-900/40 transition-all duration-200"
|
|
|
|
|
>
|
|
|
|
|
{#if loading}
|
|
|
|
|
<svg class="animate-spin w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
|
|
|
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/>
|
|
|
|
|
</svg>
|
|
|
|
|
Iniciando sesión...
|
|
|
|
|
{:else}
|
|
|
|
|
Continuar
|
|
|
|
|
{/if}
|
|
|
|
|
</Button>
|
|
|
|
|
</Field>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={goBack}
|
|
|
|
|
class="flex items-center justify-center gap-1.5 text-xs text-muted-foreground hover:text-slate-700 dark:hover:text-slate-300 transition-colors mx-auto"
|
|
|
|
|
>
|
|
|
|
|
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="sr-only">Login with Apple</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="outline" type="button" onclick={handleGoogleLogin} disabled={loading}>
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
|
|
|
<path
|
|
|
|
|
d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
|
|
|
|
fill="currentColor"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="sr-only">Login with Google</span>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="outline" type="button" onclick={handleMicrosoftLogin} disabled={loading}>
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
|
|
|
<path d="M11.4 24H0V12.6h11.4V24zM24 24H12.6V12.6H24V24zM11.4 11.4H0V0h11.4v11.4zm12.6 0H12.6V0H24v11.4z" fill="currentColor"/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span class="sr-only">Login with Microsoft</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</Field>
|
|
|
|
|
<FieldDescription class="text-center">
|
|
|
|
|
¿No tienes una cuenta? <a href="/register">Regístrate</a>
|
|
|
|
|
</FieldDescription>
|
|
|
|
|
Volver al inicio de sesión
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</FieldGroup>
|
|
|
|
|
</form>
|
|
|
|
|
<div class="relative hidden md:block bg-gradient-to-br from-blue-50 to-slate-100 dark:from-slate-900 dark:to-blue-950">
|
|
|
|
|
<div class="absolute inset-0 flex items-center justify-center p-12">
|
|
|
|
|
<div class="relative w-full h-full flex flex-col items-center justify-center gap-8">
|
|
|
|
|
<!-- Ícono principal - Escudo con documento -->
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<FileText class="w-48 h-48 text-blue-600 dark:text-blue-400 opacity-90" strokeWidth={1.5} />
|
|
|
|
|
<!-- Escudo de protección superpuesto -->
|
|
|
|
|
<div class="absolute -bottom-2 -right-2 bg-white dark:bg-slate-800 rounded-full p-2 shadow-lg">
|
|
|
|
|
<ShieldCheck class="w-12 h-12 text-green-600 dark:text-green-400" fill="currentColor" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Texto descriptivo -->
|
|
|
|
|
<div class="text-center space-y-3 max-w-sm">
|
|
|
|
|
<h2 class="text-2xl font-bold text-slate-800 dark:text-slate-100">
|
|
|
|
|
Cumplimiento Simplificado
|
|
|
|
|
</h2>
|
|
|
|
|
<p class="text-slate-600 dark:text-slate-300 text-sm leading-relaxed">
|
|
|
|
|
Gestiona tus obligaciones fiscales del Anexo 76 del SAT de manera eficiente y segura
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Elementos decorativos -->
|
|
|
|
|
<div class="absolute top-8 left-8 w-20 h-20 border-2 border-blue-200 dark:border-blue-800 rounded-full opacity-50"></div>
|
|
|
|
|
<div class="absolute bottom-8 right-8 w-16 h-16 border-2 border-blue-300 dark:border-blue-700 rounded-lg opacity-50 rotate-45"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Panel imagen -->
|
|
|
|
|
<div class="relative hidden md:block overflow-hidden">
|
|
|
|
|
<!-- Fotografía de fondo -->
|
|
|
|
|
<img
|
|
|
|
|
src="/login-bg.jpg"
|
|
|
|
|
alt=""
|
|
|
|
|
class="absolute inset-0 w-full h-full object-cover object-center"
|
|
|
|
|
/>
|
|
|
|
|
<!-- Overlay degradado -->
|
|
|
|
|
<div class="absolute inset-0 bg-gradient-to-t from-slate-900/90 via-slate-900/40 to-slate-900/10"></div>
|
|
|
|
|
|
|
|
|
|
<!-- Contenido sobre la imagen -->
|
|
|
|
|
<div class="relative z-10 h-full flex flex-col justify-end p-10">
|
|
|
|
|
<div class="flex items-center gap-3 mb-4">
|
|
|
|
|
<img src={faviconUrl} alt="" class="w-9 h-9 rounded-xl" />
|
|
|
|
|
<span class="text-white font-bold text-lg tracking-tight">Anexo 76</span>
|
|
|
|
|
</div>
|
|
|
|
|
<blockquote class="space-y-2">
|
|
|
|
|
<p class="text-white text-xl font-semibold leading-snug">
|
|
|
|
|
"Cumplimiento fiscal simplificado para empresas que operan con el SAT."
|
|
|
|
|
</p>
|
|
|
|
|
<footer class="text-slate-300 text-sm">Sistema de Cumplimiento Fiscal y Aduanal</footer>
|
|
|
|
|
</blockquote>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card.Content>
|
|
|
|
|
</Card.Root>
|
|
|
|
|
<FieldDescription class="px-6 text-center text-xs">
|
|
|
|
|
Al continuar, aceptas nuestros <a href="##" class="underline hover:text-blue-600">Términos de Servicio</a> y
|
|
|
|
|
<a href="##" class="underline hover:text-blue-600">Política de Privacidad</a>.
|
|
|
|
|
</FieldDescription>
|
|
|
|
|
<p class="px-6 text-center text-xs text-muted-foreground">
|
|
|
|
|
Al continuar, aceptas nuestros
|
|
|
|
|
<a href="##" class="underline hover:text-blue-600 underline-offset-2">Términos de Servicio</a>
|
|
|
|
|
y
|
|
|
|
|
<a href="##" class="underline hover:text-blue-600 underline-offset-2">Política de Privacidad</a>.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|