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

1062 lines
36 KiB
Plaintext
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.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { goto } from '$app/navigation';
import { auth } from '$lib/stores/auth.js';
import { toast } from '$lib/stores/toast.js';
import { onMount } from 'svelte';
let currentPassword = '';
let newPassword = '';
let confirmPassword = '';
let firstName = '';
let lastName = '';
let isUpdatingProfile = false;
let isChangingPassword = false;
let isSavingBusinessProfile = false;
let profileErrors: Record<string, string> = {};
let passwordErrors: Record<string, string> = {};
let businessProfileErrors: Record<string, string> = {};
// Tabs management
let activeTab = 'personal';
// Business profile data
let businessProfile = {
business_name: '',
commercial_name: '',
client_code: '',
client_type: '',
rfc: '',
tax_id: '',
country: 'México',
state: '',
city: '',
address: '',
external_number: '',
internal_number: '',
postal_code: '',
neighborhood: '',
main_phone: '',
secondary_phone: '',
direct_phone: '',
phone_extension: '',
fax: '',
business_hours: '',
website: '',
main_email: '',
billing_email: '',
advertising_medium: '',
nationality: 'Mexicana',
logo_url: '',
company_representative: '',
legal_representative: '',
credit_limit: '',
payment_terms: '',
preferred_currency: 'MXN',
send_to_billing: false,
is_active_client: true,
is_prospect: false,
notes: ''
};
onMount(() => {
// Redirect if not authenticated
if (!$auth.isAuthenticated) {
goto('/login');
return;
}
// Initialize form with user data
if ($auth.user) {
firstName = $auth.user.first_name;
lastName = $auth.user.last_name;
}
// Load business profile
loadBusinessProfile();
});
async function loadBusinessProfile() {
try {
const response = await fetch('/api/v1/client-profile/', {
headers: {
Authorization: `Bearer ${$auth.token}`
}
});
if (response.ok) {
const profile = await response.json();
// Fill business profile with data
Object.keys(businessProfile).forEach(key => {
if (profile[key] !== undefined && profile[key] !== null) {
businessProfile[key] = profile[key];
}
});
}
} catch (error) {
console.warn('No se pudo cargar el perfil empresarial:', error);
}
}
function validateProfileForm() {
profileErrors = {};
if (!firstName.trim()) {
profileErrors.firstName = 'El nombre es requerido';
}
if (!lastName.trim()) {
profileErrors.lastName = 'El apellido es requerido';
}
return Object.keys(profileErrors).length === 0;
}
function validatePasswordForm() {
passwordErrors = {};
if (!currentPassword) {
passwordErrors.currentPassword = 'La contraseña actual es requerida';
}
if (!newPassword) {
passwordErrors.newPassword = 'La nueva contraseña es requerida';
} else if (newPassword.length < 8) {
passwordErrors.newPassword = 'La contraseña debe tener al menos 8 caracteres';
}
if (!confirmPassword) {
passwordErrors.confirmPassword = 'Confirma la nueva contraseña';
} else if (newPassword !== confirmPassword) {
passwordErrors.confirmPassword = 'Las contraseñas no coinciden';
}
return Object.keys(passwordErrors).length === 0;
}
function validateBusinessProfile() {
businessProfileErrors = {};
// Validaciones básicas
if (businessProfile.rfc && businessProfile.rfc.length > 0) {
const rfcPattern = /^[A-ZÑ&]{3,4}[0-9]{6}[A-Z0-9]{3}$/;
if (!rfcPattern.test(businessProfile.rfc.toUpperCase())) {
businessProfileErrors.rfc = 'Formato de RFC inválido';
}
}
if (businessProfile.postal_code && businessProfile.postal_code.length > 0) {
if (!/^\d{5}$/.test(businessProfile.postal_code)) {
businessProfileErrors.postal_code = 'Código postal debe tener 5 dígitos';
}
}
if (businessProfile.main_email && businessProfile.main_email.length > 0) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(businessProfile.main_email)) {
businessProfileErrors.main_email = 'Email inválido';
}
}
if (businessProfile.billing_email && businessProfile.billing_email.length > 0) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(businessProfile.billing_email)) {
businessProfileErrors.billing_email = 'Email inválido';
}
}
return Object.keys(businessProfileErrors).length === 0;
}
async function handleProfileUpdate() {
if (!validateProfileForm()) return;
isUpdatingProfile = true;
try {
const response = await fetch('/api/v1/auth/profile', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${$auth.token}`
},
body: JSON.stringify({
first_name: firstName.trim(),
last_name: lastName.trim()
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Error al actualizar perfil');
}
const updatedUser = await response.json();
auth.updateUser(updatedUser);
toast.success('Perfil actualizado exitosamente');
} catch (error: any) {
toast.error(error.message || 'Error al actualizar perfil');
} finally {
isUpdatingProfile = false;
}
}
async function handlePasswordChange() {
if (!validatePasswordForm()) return;
isChangingPassword = true;
try {
const response = await fetch('/api/v1/auth/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${$auth.token}`
},
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Error al cambiar contraseña');
}
// Clear form
currentPassword = '';
newPassword = '';
confirmPassword = '';
toast.success('Contraseña cambiada exitosamente');
} catch (error: any) {
toast.error(error.message || 'Error al cambiar contraseña');
} finally {
isChangingPassword = false;
}
}
async function handleBusinessProfileSave() {
if (!validateBusinessProfile()) return;
isSavingBusinessProfile = true;
try {
// Prepare data - remove empty strings and convert types
const profileData = { ...businessProfile };
// Clean up empty strings
Object.keys(profileData).forEach(key => {
if (profileData[key] === '') {
profileData[key] = null;
}
});
// Convert credit_limit to number if provided
if (profileData.credit_limit) {
profileData.credit_limit = parseFloat(profileData.credit_limit);
}
const response = await fetch('/api/v1/client-profile/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${$auth.token}`
},
body: JSON.stringify(profileData)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Error al guardar perfil empresarial');
}
const savedProfile = await response.json();
toast.success('Perfil empresarial guardado exitosamente');
// Update local data
Object.keys(businessProfile).forEach(key => {
if (savedProfile[key] !== undefined && savedProfile[key] !== null) {
businessProfile[key] = savedProfile[key];
}
});
} catch (error: any) {
toast.error(error.message || 'Error al guardar perfil empresarial');
} finally {
isSavingBusinessProfile = false;
}
}
</script>
<svelte:head>
<title>Mi Perfil - ServiceManager</title>
</svelte:head>
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Header -->
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Mi Perfil</h1>
<p class="text-gray-600 mt-2">Gestiona tu información personal y configuración empresarial</p>
</div>
<!-- Tabs Navigation -->
<div class="border-b border-gray-200 mb-8">
<nav class="-mb-px flex space-x-8">
<button
class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors {activeTab ===
'personal'
? 'border-primary-500 text-primary-600'
: ''}"
on:click={() => (activeTab = 'personal')}
>
👤 Personal
</button>
<button
class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors {activeTab ===
'general'
? 'border-primary-500 text-primary-600'
: ''}"
on:click={() => (activeTab = 'general')}
>
🏢 General
</button>
<button
class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors {activeTab ===
'contact'
? 'border-primary-500 text-primary-600'
: ''}"
on:click={() => (activeTab = 'contact')}
>
📞 Contacto
</button>
<button
class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors {activeTab ===
'security'
? 'border-primary-500 text-primary-600'
: ''}"
on:click={() => (activeTab = 'security')}
>
🔐 Seguridad
</button>
<button
class="border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors {activeTab ===
'account'
? 'border-primary-500 text-primary-600'
: ''}"
on:click={() => (activeTab = 'account')}
>
Cuenta
</button>
</nav>
</div>
<!-- Tab Content -->
<div class="space-y-8">
<!-- Personal Information Tab -->
{#if activeTab === 'personal'}
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold text-gray-900">Información Personal</h2>
<p class="text-gray-600 mt-1">Actualiza tu información básica</p>
</div>
<div class="card-content">
<form on:submit|preventDefault={handleProfileUpdate} class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="first-name" class="form-label">
Nombre <span class="text-red-500">*</span>
</label>
<input
id="first-name"
type="text"
class="form-input {profileErrors.firstName ? 'border-red-300' : ''}"
bind:value={firstName}
disabled={isUpdatingProfile}
/>
{#if profileErrors.firstName}
<p class="form-error">{profileErrors.firstName}</p>
{/if}
</div>
<div>
<label for="last-name" class="form-label">
Apellido <span class="text-red-500">*</span>
</label>
<input
id="last-name"
type="text"
class="form-input {profileErrors.lastName ? 'border-red-300' : ''}"
bind:value={lastName}
disabled={isUpdatingProfile}
/>
{#if profileErrors.lastName}
<p class="form-error">{profileErrors.lastName}</p>
{/if}
</div>
</div>
<div>
<label for="email" class="form-label">Correo Electrónico</label>
<input
id="email"
type="email"
class="form-input bg-gray-50"
value={$auth.user?.email || ''}
disabled
readonly
/>
<p class="text-sm text-gray-500 mt-1">
El correo electrónico no puede ser modificado. Contacta al administrador si
necesitas cambiarlo.
</p>
</div>
<div class="flex justify-end">
<button type="submit" class="btn-primary px-6 py-2" disabled={isUpdatingProfile}>
{#if isUpdatingProfile}
<div class="flex items-center space-x-2">
<div class="spinner w-4 h-4" />
<span>Guardando...</span>
</div>
{:else}
Guardar Cambios
{/if}
</button>
</div>
</form>
</div>
</div>
{/if}
<!-- General Business Information Tab -->
{#if activeTab === 'general'}
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold text-gray-900">Información Empresarial</h2>
<p class="text-gray-600 mt-1">Datos generales de tu empresa</p>
</div>
<div class="card-content">
<form on:submit|preventDefault={handleBusinessProfileSave} class="space-y-6">
<!-- Información General -->
<div class="bg-gray-50 p-4 rounded-lg">
<h3 class="font-medium text-gray-900 mb-4">Información General</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="form-label">Razón Social</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.business_name}
placeholder="Empresa S.A. de C.V."
/>
</div>
<div>
<label class="form-label">Nombre Comercial</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.commercial_name}
placeholder="Mi Empresa"
/>
</div>
<div>
<label class="form-label">Clave de Cliente</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.client_code}
placeholder="CLI001"
/>
</div>
<div>
<label class="form-label">Tipo de Cliente</label>
<select class="form-input" bind:value={businessProfile.client_type}>
<option value="">Seleccionar...</option>
<option value="corporativo">Corporativo</option>
<option value="pyme">PyME</option>
<option value="startup">Startup</option>
<option value="gobierno">Gobierno</option>
<option value="ong">ONG</option>
</select>
</div>
<div>
<label class="form-label">RFC</label>
<input
type="text"
class="form-input {businessProfileErrors.rfc ? 'border-red-300' : ''}"
bind:value={businessProfile.rfc}
placeholder="XAXX010101000"
maxlength="13"
style="text-transform: uppercase"
/>
{#if businessProfileErrors.rfc}
<p class="form-error">{businessProfileErrors.rfc}</p>
{/if}
</div>
<div>
<label class="form-label">ID Fiscal (Otros países)</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.tax_id}
placeholder="Tax ID / VAT Number"
/>
</div>
</div>
</div>
<!-- Ubicación -->
<div class="bg-blue-50 p-4 rounded-lg">
<h3 class="font-medium text-gray-900 mb-4">Ubicación</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>
<label class="form-label">País</label>
<input type="text" class="form-input" bind:value={businessProfile.country} />
</div>
<div>
<label class="form-label">Estado</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.state}
placeholder="Chihuahua"
/>
</div>
<div>
<label class="form-label">Ciudad</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.city}
placeholder="Ciudad Juárez"
/>
</div>
<div class="md:col-span-2">
<label class="form-label">Dirección</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.address}
placeholder="Av. Principal 123"
/>
</div>
<div>
<label class="form-label">Número Exterior</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.external_number}
placeholder="123"
/>
</div>
<div>
<label class="form-label">Número Interior</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.internal_number}
placeholder="A"
/>
</div>
<div>
<label class="form-label">Código Postal</label>
<input
type="text"
class="form-input {businessProfileErrors.postal_code ? 'border-red-300' : ''}"
bind:value={businessProfile.postal_code}
placeholder="32000"
maxlength="5"
/>
{#if businessProfileErrors.postal_code}
<p class="form-error">{businessProfileErrors.postal_code}</p>
{/if}
</div>
<div>
<label class="form-label">Colonia</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.neighborhood}
placeholder="Centro"
/>
</div>
</div>
</div>
<!-- Representantes -->
<div class="bg-purple-50 p-4 rounded-lg">
<h3 class="font-medium text-gray-900 mb-4">Representantes</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="form-label">Encargado/Representante</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.company_representative}
placeholder="Juan Pérez"
/>
</div>
<div>
<label class="form-label">Representante Legal</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.legal_representative}
placeholder="María González"
/>
</div>
</div>
</div>
<!-- Configuración -->
<div class="bg-green-50 p-4 rounded-lg">
<h3 class="font-medium text-gray-900 mb-4">Configuración</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="form-label">Moneda Preferida</label>
<select class="form-input" bind:value={businessProfile.preferred_currency}>
<option value="MXN">MXN - Peso Mexicano</option>
<option value="USD">USD - Dólar Americano</option>
<option value="EUR">EUR - Euro</option>
</select>
</div>
<div>
<label class="form-label">Nacionalidad</label>
<input type="text" class="form-input" bind:value={businessProfile.nationality} />
</div>
<div class="md:col-span-2">
<label class="form-label">Notas Adicionales</label>
<textarea
class="form-input"
rows="3"
bind:value={businessProfile.notes}
placeholder="Información adicional sobre la empresa..."
/>
</div>
</div>
<div class="mt-4 space-y-3">
<label class="flex items-center space-x-2">
<input
type="checkbox"
class="form-checkbox"
bind:checked={businessProfile.send_to_billing}
/>
<span class="text-sm text-gray-700">Enviar a Facturación</span>
</label>
<label class="flex items-center space-x-2">
<input
type="checkbox"
class="form-checkbox"
bind:checked={businessProfile.is_prospect}
/>
<span class="text-sm text-gray-700">Es Prospecto</span>
</label>
</div>
</div>
<div class="flex justify-end pt-6">
<button
type="submit"
class="btn-primary px-8 py-2"
disabled={isSavingBusinessProfile}
>
{#if isSavingBusinessProfile}
<div class="flex items-center space-x-2">
<div class="spinner w-4 h-4" />
<span>Guardando...</span>
</div>
{:else}
💾 Guardar Perfil Empresarial
{/if}
</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Contact Information Tab -->
{#if activeTab === 'contact'}
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold text-gray-900">Información de Contacto</h2>
<p class="text-gray-600 mt-1">Datos de contacto y comunicación</p>
</div>
<div class="card-content">
<form on:submit|preventDefault={handleBusinessProfileSave} class="space-y-6">
<!-- Teléfonos -->
<div class="bg-blue-50 p-4 rounded-lg">
<h3 class="font-medium text-gray-900 mb-4">📞 Teléfonos</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="form-label">Teléfono Principal</label>
<input
type="tel"
class="form-input"
bind:value={businessProfile.main_phone}
placeholder="+52 656 123 4567"
/>
</div>
<div>
<label class="form-label">Teléfono Secundario</label>
<input
type="tel"
class="form-input"
bind:value={businessProfile.secondary_phone}
placeholder="+52 656 123 4568"
/>
</div>
<div>
<label class="form-label">Teléfono Directo</label>
<input
type="tel"
class="form-input"
bind:value={businessProfile.direct_phone}
placeholder="+52 656 123 4569"
/>
</div>
<div>
<label class="form-label">Extensión</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.phone_extension}
placeholder="101"
maxlength="10"
/>
</div>
<div>
<label class="form-label">Fax</label>
<input
type="tel"
class="form-input"
bind:value={businessProfile.fax}
placeholder="+52 656 123 4570"
/>
</div>
</div>
</div>
<!-- Emails -->
<div class="bg-green-50 p-4 rounded-lg">
<h3 class="font-medium text-gray-900 mb-4">✉️ Correos Electrónicos</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="form-label">Email Principal</label>
<input
type="email"
class="form-input {businessProfileErrors.main_email ? 'border-red-300' : ''}"
bind:value={businessProfile.main_email}
placeholder="contacto@empresa.com"
/>
{#if businessProfileErrors.main_email}
<p class="form-error">{businessProfileErrors.main_email}</p>
{/if}
</div>
<div>
<label class="form-label">Email de Facturación</label>
<input
type="email"
class="form-input {businessProfileErrors.billing_email ? 'border-red-300' : ''}"
bind:value={businessProfile.billing_email}
placeholder="facturacion@empresa.com"
/>
{#if businessProfileErrors.billing_email}
<p class="form-error">{businessProfileErrors.billing_email}</p>
{/if}
</div>
</div>
</div>
<!-- Web y Horarios -->
<div class="bg-purple-50 p-4 rounded-lg">
<h3 class="font-medium text-gray-900 mb-4">🌐 Web y Horarios</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="form-label">Página Web</label>
<input
type="url"
class="form-input"
bind:value={businessProfile.website}
placeholder="https://www.empresa.com"
/>
</div>
<div>
<label class="form-label">Horario de Atención</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.business_hours}
placeholder="Lun-Vie 9:00-18:00"
/>
</div>
<div class="md:col-span-2">
<label class="form-label">Medio de Publicidad</label>
<input
type="text"
class="form-input"
bind:value={businessProfile.advertising_medium}
placeholder="¿Cómo nos conoció?"
/>
</div>
</div>
</div>
<div class="flex justify-end pt-6">
<button
type="submit"
class="btn-primary px-8 py-2"
disabled={isSavingBusinessProfile}
>
{#if isSavingBusinessProfile}
<div class="flex items-center space-x-2">
<div class="spinner w-4 h-4" />
<span>Guardando...</span>
</div>
{:else}
💾 Guardar Información de Contacto
{/if}
</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Security Tab -->
{#if activeTab === 'security'}
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold text-gray-900">Configuración de Seguridad</h2>
<p class="text-gray-600 mt-1">Gestiona tu contraseña y configuración de seguridad</p>
</div>
<div class="card-content space-y-6">
<!-- Two-Factor Authentication Status -->
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
<div>
<h3 class="font-medium text-gray-900">Autenticación de dos factores (2FA)</h3>
<p class="text-sm text-gray-600">
{$auth.user?.is_two_factor_enabled
? 'La autenticación de dos factores está habilitada'
: 'Mejora la seguridad habilitando 2FA'}
</p>
</div>
<div>
{#if $auth.user?.is_two_factor_enabled}
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800"
>
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
Habilitado
</span>
{:else}
<span
class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800"
>
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
Deshabilitado
</span>
{/if}
</div>
</div>
<!-- Change Password Form -->
<form on:submit|preventDefault={handlePasswordChange} class="space-y-6">
<h3 class="text-lg font-medium text-gray-900">Cambiar Contraseña</h3>
<div>
<label for="current-password" class="form-label">
Contraseña Actual <span class="text-red-500">*</span>
</label>
<input
id="current-password"
type="password"
class="form-input {passwordErrors.currentPassword ? 'border-red-300' : ''}"
bind:value={currentPassword}
disabled={isChangingPassword}
/>
{#if passwordErrors.currentPassword}
<p class="form-error">{passwordErrors.currentPassword}</p>
{/if}
</div>
<div>
<label for="new-password" class="form-label">
Nueva Contraseña <span class="text-red-500">*</span>
</label>
<input
id="new-password"
type="password"
class="form-input {passwordErrors.newPassword ? 'border-red-300' : ''}"
bind:value={newPassword}
disabled={isChangingPassword}
/>
{#if passwordErrors.newPassword}
<p class="form-error">{passwordErrors.newPassword}</p>
{/if}
</div>
<div>
<label for="confirm-password" class="form-label">
Confirmar Nueva Contraseña <span class="text-red-500">*</span>
</label>
<input
id="confirm-password"
type="password"
class="form-input {passwordErrors.confirmPassword ? 'border-red-300' : ''}"
bind:value={confirmPassword}
disabled={isChangingPassword}
/>
{#if passwordErrors.confirmPassword}
<p class="form-error">{passwordErrors.confirmPassword}</p>
{/if}
</div>
<div class="flex justify-end">
<button type="submit" class="btn-primary px-6 py-2" disabled={isChangingPassword}>
{#if isChangingPassword}
<div class="flex items-center space-x-2">
<div class="spinner w-4 h-4" />
<span>Cambiando...</span>
</div>
{:else}
Cambiar Contraseña
{/if}
</button>
</div>
</form>
</div>
</div>
{/if}
<!-- Account Information Tab -->
{#if activeTab === 'account'}
<div class="card">
<div class="card-header">
<h2 class="text-xl font-semibold text-gray-900">Información de la Cuenta</h2>
<p class="text-gray-600 mt-1">Detalles sobre tu cuenta y organización</p>
</div>
<div class="card-content">
<dl class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<dt class="text-sm font-medium text-gray-500">ID de Usuario</dt>
<dd class="text-sm text-gray-900 font-mono mt-1">
#{$auth.user?.id.substring(0, 8)}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Rol</dt>
<dd class="text-sm text-gray-900 mt-1">
{$auth.user?.role === 'CLIENT_ADMIN'
? 'Administrador de Cliente'
: 'Usuario de Cliente'}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Estado de la Cuenta</dt>
<dd class="text-sm mt-1">
{#if $auth.user?.is_active}
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
>
Activa
</span>
{:else}
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800"
>
Inactiva
</span>
{/if}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Miembro desde</dt>
<dd class="text-sm text-gray-900 mt-1">
{$auth.user?.created_at
? new Date($auth.user.created_at).toLocaleDateString('es-ES', {
day: '2-digit',
month: 'long',
year: 'numeric'
})
: 'N/A'}
</dd>
</div>
</dl>
</div>
</div>
{/if}
</div>
</div>
<style>
.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.form-checkbox {
@apply rounded border-gray-300 text-primary-600 focus:ring-primary-500 focus:border-primary-500;
}
</style>