Feature: Formulario de edición en lugar de modal
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 57s
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:
@@ -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: [
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
325
nuxt4/app/components/UserProfileForm.vue
Normal file
325
nuxt4/app/components/UserProfileForm.vue
Normal 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>
|
||||||
Reference in New Issue
Block a user