Feature: Formulario de edición en lugar de modal
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 57s

- Crear componente UserProfileForm con diseño glassmorphism
- Alternar entre lista de apps y formulario de edición
- Quitar modal de UserHeader, usar emit event
- Agregar nuevos campos deshabilitados:
  * Avatar URL
  * Teléfono
  * Cédula
  * Fecha de nacimiento
  * NFC vinculada
  * PIN numérico
  * Código Nucleo V2
- Sistema de eventos entre componentes
This commit is contained in:
2025-10-16 23:12:46 -06:00
parent 9a3dc1f0e6
commit 44c4727588
3 changed files with 338 additions and 126 deletions

View File

@@ -11,10 +11,14 @@
<UContainer class="py-8"> <UContainer class="py-8">
<div v-if="isAuthenticated" class="space-y-6"> <div v-if="isAuthenticated" class="space-y-6">
<!-- Header principal con info del usuario --> <!-- Header principal con info del usuario -->
<UserHeader /> <UserHeader @edit-profile="showProfileForm = true" />
<!-- Lista de aplicaciones --> <!-- Formulario de edición de perfil o Lista de aplicaciones -->
<AuthApplicationsList /> <UserProfileForm
v-if="showProfileForm"
@close="showProfileForm = false"
/>
<AuthApplicationsList v-else />
<!-- Acciones rápidas en footer transparente --> <!-- Acciones rápidas en footer transparente -->
<div class="quick-actions"> <div class="quick-actions">
@@ -45,6 +49,9 @@
const { isAuthenticated } = useAuthentik() const { isAuthenticated } = useAuthentik()
const { isNight } = useTheme() const { isNight } = useTheme()
// Estado para mostrar formulario de edición
const showProfileForm = ref(false)
// Configurar meta tags para PWA // Configurar meta tags para PWA
useHead({ useHead({
link: [ link: [

View File

@@ -17,7 +17,7 @@
<div class="user-info"> <div class="user-info">
<div class="user-name-row"> <div class="user-name-row">
<h1 class="user-name">{{ user?.name || user?.username }}</h1> <h1 class="user-name">{{ user?.name || user?.username }}</h1>
<button class="edit-button" @click.stop="openEditProfile" title="Editar perfil"> <button class="edit-button" @click="$emit('edit-profile')" title="Editar perfil">
<UIcon name="i-heroicons-pencil-square" class="w-4 h-4" /> <UIcon name="i-heroicons-pencil-square" class="w-4 h-4" />
</button> </button>
</div> </div>
@@ -53,135 +53,15 @@
<UIcon v-else key="sun" name="i-heroicons-sun" class="w-6 h-6" /> <UIcon v-else key="sun" name="i-heroicons-sun" class="w-6 h-6" />
</transition> </transition>
</button> </button>
<!-- Modal de edición de perfil -->
<UModal v-model:open="isEditModalOpen" title="Editar Perfil">
<template #content>
<div class="p-4 space-y-4">
<UFormGroup label="Nombre de usuario" name="username">
<UInput
:model-value="user?.username"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<template #help>
<span class="text-xs text-gray-500">El username no se puede cambiar</span>
</template>
</UFormGroup>
<UFormGroup label="Nombre completo" name="name" required>
<UInput
v-model="formData.name"
placeholder="Tu nombre completo"
:disabled="isSubmitting"
/>
</UFormGroup>
<UFormGroup label="Email" name="email" required>
<UInput
v-model="formData.email"
type="email"
placeholder="tu@email.com"
:disabled="isSubmitting"
/>
</UFormGroup>
<div class="flex justify-end gap-3 pt-4">
<UButton
color="neutral"
variant="ghost"
@click="isEditModalOpen = false"
:disabled="isSubmitting"
>
Cancelar
</UButton>
<UButton
color="primary"
:loading="isSubmitting"
@click="handleSubmit"
>
Guardar cambios
</UButton>
</div>
</div>
</template>
</UModal>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const { user } = useAuthentik() const { user } = useAuthentik()
const { isNight, toggleTheme } = useTheme() const { isNight, toggleTheme } = useTheme()
const toast = useToast()
// Estado del modal // Emits
const isEditModalOpen = ref(false) defineEmits(['edit-profile'])
const isSubmitting = ref(false)
// Datos del formulario
const formData = ref({
name: user.value?.name || '',
email: user.value?.email || ''
})
// Abrir modal de edición
const openEditProfile = () => {
if (user.value) {
formData.value = {
name: user.value.name || '',
email: user.value.email || ''
}
isEditModalOpen.value = true
}
}
// Enviar formulario
const handleSubmit = async () => {
if (!formData.value.name || !formData.value.email) {
toast.add({
title: 'Error',
description: 'Por favor completa todos los campos',
color: 'error',
icon: 'i-heroicons-exclamation-triangle'
})
return
}
isSubmitting.value = true
try {
await $fetch('/api/authentik/user', {
method: 'PATCH',
body: {
name: formData.value.name,
email: formData.value.email
}
})
toast.add({
title: 'Perfil actualizado',
description: 'Tus cambios se guardaron correctamente. Recarga la página para verlos.',
color: 'success',
icon: 'i-heroicons-check-circle',
actions: [{
label: 'Recargar',
onClick: () => window.location.reload()
}]
})
isEditModalOpen.value = false
} catch (error) {
console.error('Error updating profile:', error)
toast.add({
title: 'Error',
description: 'No se pudo actualizar el perfil',
color: 'error',
icon: 'i-heroicons-x-circle'
})
} finally {
isSubmitting.value = false
}
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -0,0 +1,325 @@
<template>
<div class="profile-form-container">
<div class="form-header">
<div class="flex items-center justify-between mb-4">
<h2 class="form-title">
<UIcon name="i-heroicons-user-circle" class="w-6 h-6" />
Editar Perfil
</h2>
<UButton
color="neutral"
variant="ghost"
icon="i-heroicons-x-mark"
@click="$emit('close')"
>
Cancelar
</UButton>
</div>
</div>
<form @submit.prevent="handleSubmit" class="form-content">
<div class="form-grid">
<!-- Nombre de usuario (readonly) -->
<UFormGroup label="Nombre de usuario" name="username">
<UInput
:model-value="user?.username"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<template #help>
<span class="text-xs text-gray-500">El nombre de usuario no se puede cambiar</span>
</template>
</UFormGroup>
<!-- Nombre completo -->
<UFormGroup label="Nombre completo" name="name" required>
<UInput
v-model="formData.name"
placeholder="Tu nombre completo"
:disabled="isSubmitting"
/>
</UFormGroup>
<!-- Email -->
<UFormGroup label="Correo electrónico" name="email" required>
<UInput
v-model="formData.email"
type="email"
placeholder="tu@email.com"
:disabled="isSubmitting"
/>
</UFormGroup>
<!-- Avatar URL (deshabilitado) -->
<UFormGroup label="URL del avatar" name="avatar">
<UInput
v-model="formData.avatar"
placeholder="https://ejemplo.com/avatar.jpg"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<template #help>
<span class="text-xs text-gray-500">Próximamente disponible</span>
</template>
</UFormGroup>
<!-- Teléfono (deshabilitado) -->
<UFormGroup label="Teléfono" name="phone">
<UInput
v-model="formData.phone"
placeholder="+506 1234-5678"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<template #help>
<span class="text-xs text-gray-500">Próximamente disponible</span>
</template>
</UFormGroup>
<!-- Cédula (deshabilitado) -->
<UFormGroup label="Cédula" name="cedula">
<UInput
v-model="formData.cedula"
placeholder="1-2345-6789"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<template #help>
<span class="text-xs text-gray-500">Próximamente disponible</span>
</template>
</UFormGroup>
<!-- Fecha de nacimiento (deshabilitado) -->
<UFormGroup label="Fecha de nacimiento" name="birthdate">
<UInput
v-model="formData.birthdate"
type="date"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<template #help>
<span class="text-xs text-gray-500">Próximamente disponible</span>
</template>
</UFormGroup>
<!-- NFC vinculada (deshabilitado) -->
<UFormGroup label="NFC vinculada" name="nfc">
<UInput
v-model="formData.nfc"
placeholder="ID de tarjeta NFC"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<template #help>
<span class="text-xs text-gray-500">Próximamente disponible</span>
</template>
</UFormGroup>
<!-- PIN numérico (deshabilitado) -->
<UFormGroup label="PIN numérico" name="pin">
<UInput
v-model="formData.pin"
type="password"
placeholder="••••"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<template #help>
<span class="text-xs text-gray-500">Próximamente disponible</span>
</template>
</UFormGroup>
<!-- Código Nucleo V2 (deshabilitado) -->
<UFormGroup label="Código Nucleo V2" name="nucleoCode">
<UInput
v-model="formData.nucleoCode"
placeholder="NUCLEO-XXXX-XXXX"
disabled
:ui="{ base: 'cursor-not-allowed opacity-50' }"
/>
<template #help>
<span class="text-xs text-gray-500">Próximamente disponible</span>
</template>
</UFormGroup>
</div>
<!-- Botones de acción -->
<div class="form-actions">
<UButton
color="neutral"
variant="ghost"
size="lg"
@click="$emit('close')"
:disabled="isSubmitting"
>
Cancelar
</UButton>
<UButton
color="primary"
size="lg"
type="submit"
:loading="isSubmitting"
>
Guardar cambios
</UButton>
</div>
</form>
</div>
</template>
<script setup lang="ts">
const { user } = useAuthentik()
const toast = useToast()
// Emits
defineEmits(['close'])
// Estado del formulario
const isSubmitting = ref(false)
// Datos del formulario
const formData = ref({
name: user.value?.name || '',
email: user.value?.email || '',
avatar: '',
phone: '',
cedula: '',
birthdate: '',
nfc: '',
pin: '',
nucleoCode: ''
})
// Enviar formulario
const handleSubmit = async () => {
if (!formData.value.name || !formData.value.email) {
toast.add({
title: 'Error',
description: 'Por favor completa todos los campos requeridos',
color: 'error',
icon: 'i-heroicons-exclamation-triangle'
})
return
}
isSubmitting.value = true
try {
await $fetch('/api/authentik/user', {
method: 'PATCH',
body: {
name: formData.value.name,
email: formData.value.email
}
})
toast.add({
title: 'Perfil actualizado',
description: 'Tus cambios se guardaron correctamente. Recarga la página para verlos.',
color: 'success',
icon: 'i-heroicons-check-circle',
actions: [{
label: 'Recargar',
onClick: () => window.location.reload()
}]
})
} catch (error) {
console.error('Error updating profile:', error)
toast.add({
title: 'Error',
description: 'No se pudo actualizar el perfil',
color: 'error',
icon: 'i-heroicons-x-circle'
})
} finally {
isSubmitting.value = false
}
}
</script>
<style scoped>
.profile-form-container {
background: rgba(255, 255, 255, 0.35);
backdrop-filter: blur(20px) saturate(180%);
border-radius: 1.5rem;
padding: 2rem;
box-shadow:
0 8px 32px 0 rgba(31, 38, 135, 0.15),
inset 0 1px 1px 0 rgba(255, 255, 255, 0.3);
border: 1px solid rgba(255, 255, 255, 0.18);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.form-header {
margin-bottom: 2rem;
}
.form-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.75rem;
font-weight: 700;
color: var(--color-gray-900);
margin: 0;
}
.form-content {
display: flex;
flex-direction: column;
gap: 2rem;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
/* Responsive */
@media (max-width: 768px) {
.profile-form-container {
padding: 1.5rem;
}
.form-title {
font-size: 1.5rem;
}
.form-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
.form-actions {
flex-direction: column;
}
}
</style>
<style>
/* Estilos de modo oscuro (sin scoped para que .dark funcione correctamente) */
.dark .profile-form-container {
background: rgba(0, 0, 0, 0.15) !important;
box-shadow:
0 8px 32px 0 rgba(0, 0, 0, 0.5),
inset 0 1px 1px 0 rgba(255, 255, 255, 0.05) !important;
border: 1px solid rgba(255, 255, 255, 0.08) !important;
}
.dark .form-title {
color: var(--color-gray-100) !important;
}
.dark .form-actions {
border-top-color: rgba(255, 255, 255, 0.1);
}
</style>