Files
service_manager/frontend-client/src/routes/login/+page.svelte

277 lines
10 KiB
Svelte
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { auth } from '$lib/stores/auth.js';
import { toast } from '$lib/stores/toast.js';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import Icon from '$lib/components/Icon.svelte';
let email = '';
let password = '';
let tenantSlug = 'ventas';
let totpCode = '';
let isLoading = false;
let showTwoFactor = false;
let errorMessage = '';
let showPassword = false;
onMount(() => {
// Redirect if already authenticated
if ($auth.isAuthenticated) {
goto('/');
}
});
async function handleLogin() {
if (!email || !password) {
errorMessage = 'Por favor completa todos los campos';
return;
}
isLoading = true;
errorMessage = '';
try {
await auth.login({
email,
password,
tenant_slug: tenantSlug.trim() || 'ventas',
totp_code: totpCode || undefined
});
toast.success('¡Bienvenido! Has iniciado sesión correctamente');
goto('/');
} catch (error: any) {
console.error('Login error:', error);
// Check if 2FA is required
if (error.message.includes('two-factor') || error.message.includes('2FA')) {
showTwoFactor = true;
errorMessage = 'Introduce el código de tu aplicación de autenticación';
} else {
errorMessage = error.message || 'Error al iniciar sesión';
toast.error(errorMessage);
}
} finally {
isLoading = false;
}
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter') {
handleLogin();
}
}
</script>
<div class="min-h-screen flex font-sans bg-white overflow-hidden">
<!-- Left Side: Hero Image & Overlay (55% width) -->
<div class="hidden lg:flex w-[55%] relative bg-gray-900">
<!-- Background Image -->
<div
class="absolute inset-0 bg-cover bg-center z-0"
style="background-image: url('/images/SOPORTE.webp'); opacity: 1;"
/>
<!-- Gradient Overlay -->
<div class="absolute inset-0 bg-gradient-to-br from-[#1e3a8a]/75 to-[#172554]/75 z-10" />
<div
class="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent z-10"
/>
<!-- Content -->
<div class="relative z-20 w-full h-full flex flex-col justify-between p-16 text-white">
<!-- Top Logo (Left) -->
<div class="flex flex-col">
<img
src="/images/Logo%20AS%20blanco(1).png"
alt="AduanaSoft"
class="h-32 w-auto object-contain self-start drop-shadow-lg"
/>
</div>
<!-- Main Hero Text -->
<div class="space-y-4 mb-12">
<h2 class="text-5xl font-extrabold tracking-tight drop-shadow-xl leading-tight">
Control Total <br />
de Servicios de TI
</h2>
<p class="text-lg text-blue-100/90 font-light max-w-lg leading-relaxed drop-shadow-md">
Portal de atención a clientes. Genere tickets de soporte técnico para nuestros sistemas y
reciba asistencia especializada para garantizar la continuidad de su operación.
</p>
</div>
<!-- Bottom Footer -->
<div class="text-xs font-bold tracking-[0.2em] text-blue-200/60 uppercase">
ServiceManager Enterprise Platform
</div>
</div>
</div>
<!-- Right Side: Login Form (45% width) -->
<div
class="w-full lg:w-[45%] flex flex-col justify-center items-center p-8 lg:p-16 bg-white relative"
>
<div class="w-full max-w-md space-y-8">
<!-- Logo & Header -->
<div class="text-center space-y-2">
<h2 class="text-3xl font-bold text-gray-900">Bienvenido</h2>
<p class="text-gray-500 text-sm">Ingrese a su cuenta corporativa</p>
</div>
<!-- Form -->
<form on:submit|preventDefault={handleLogin} class="space-y-6 mt-8">
{#if errorMessage}
<div
class="p-3 rounded-md bg-red-50 border border-red-100 flex items-center gap-3 animate-fade-in text-sm text-red-600"
>
<Icon name="alert-circle" className="w-4 h-4 flex-shrink-0" />
{errorMessage}
</div>
{/if}
{#if !showTwoFactor}
<div class="space-y-5">
<!-- Email Input -->
<div class="space-y-1.5">
<label for="email" class="block text-sm font-semibold text-gray-700"
>Correo Electrónico</label
>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon
name="mail"
className="w-5 h-5 text-gray-400 group-focus-within:text-blue-600 transition-colors"
/>
</div>
<input
id="email"
type="email"
bind:value={email}
on:keydown={handleKeyDown}
class="block w-full pl-10 pr-3 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
placeholder="admin@aduanasoft.com"
required
disabled={isLoading}
/>
</div>
</div>
<!-- Password Input -->
<div class="space-y-1.5">
<label for="password" class="block text-sm font-semibold text-gray-700"
>Contraseña</label
>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon
name="lock"
className="w-5 h-5 text-gray-400 group-focus-within:text-blue-600 transition-colors"
/>
</div>
{#if showPassword}
<input
id="password"
type="text"
bind:value={password}
on:keydown={handleKeyDown}
class="block w-full pl-10 pr-10 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
placeholder="••••••••"
required
disabled={isLoading}
/>
{:else}
<input
id="password"
type="password"
bind:value={password}
on:keydown={handleKeyDown}
class="block w-full pl-10 pr-10 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
placeholder="••••••••"
required
disabled={isLoading}
/>
{/if}
<button
type="button"
class="absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer text-gray-400 hover:text-gray-600 focus:outline-none"
on:click={() => (showPassword = !showPassword)}
>
<Icon name={showPassword ? 'eye-off' : 'eye'} className="w-5 h-5" />
</button>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<input
id="remember-me"
name="remember-me"
type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded cursor-pointer"
/>
<label
for="remember-me"
class="ml-2 block text-sm text-gray-500 cursor-pointer select-none"
>Recordar en este equipo</label
>
</div>
<button
type="button"
class="text-sm font-medium text-blue-600 hover:text-blue-500 bg-transparent border-none p-0 cursor-pointer"
on:click={() => goto('/forgot-password')}
>
Olvidé mi clave
</button>
</div>
</div>
{:else}
<!-- 2FA Input -->
<div class="space-y-4 animate-slide-up">
<label for="code" class="block text-sm font-medium text-gray-700 text-center"
>Código de Verificación (2FA)</label
>
<p class="text-xs text-center text-gray-500 mb-4">Ingrese el código de 6 dígitos</p>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Icon name="shield-check" className="w-5 h-5 text-blue-500" />
</div>
<input
id="code"
type="text"
bind:value={totpCode}
on:keydown={handleKeyDown}
class="block w-full pl-10 py-3 text-center tracking-[0.5em] font-mono text-lg border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent"
placeholder="000000"
maxlength="6"
required
disabled={isLoading}
/>
</div>
</div>
{/if}
<div class="pt-2">
<button
type="submit"
class="w-full flex justify-center py-3.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-bold text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
disabled={isLoading}
>
{#if isLoading}
<Icon name="loader-2" className="w-5 h-5 animate-spin mr-2" />
Procesando...
{:else}
{showTwoFactor ? 'Verificar Acceso' : 'Acceder al Portal'}
{/if}
</button>
</div>
<div class="mt-8 text-center text-xs text-gray-400">
© 2026 Aduanasoft. Acceso exclusivo autorizado.
</div>
</form>
</div>
</div>
</div>