Feat: Persistencia de cambios y alertas al salir del formulario
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
- Agregar cookie para persistir cambios del formulario (7 días) - Detectar y mostrar campos modificados con indicadores visuales - Alertar al usuario antes de salir si hay cambios sin guardar - Restaurar borrador automáticamente al reabrir formulario - Notificar al usuario de cambios restaurados con toast - Limpiar cookie al guardar exitosamente Indicadores visuales: - Label en color warning para campos modificados - Badge "Modificado" con ícono de lápiz - Mensaje en beforeunload listando campos pendientes La cookie guarda todos los campos del formulario y se restauran automáticamente al abrir el formulario de nuevo.
This commit is contained in:
@@ -86,10 +86,14 @@
|
|||||||
|
|
||||||
<!-- Nombre completo -->
|
<!-- Nombre completo -->
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="field-label">
|
<label class="field-label" :class="{ 'field-modified': modifiedFields.name }">
|
||||||
<UIcon name="i-heroicons-user" class="w-4 h-4" />
|
<UIcon name="i-heroicons-user" class="w-4 h-4" />
|
||||||
Nombre completo
|
Nombre completo
|
||||||
<span class="required">*</span>
|
<span class="required">*</span>
|
||||||
|
<span v-if="modifiedFields.name" class="modified-indicator">
|
||||||
|
<UIcon name="i-heroicons-pencil-square" class="w-3 h-3" />
|
||||||
|
Modificado
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="formData.name"
|
v-model="formData.name"
|
||||||
@@ -100,10 +104,14 @@
|
|||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="field-label">
|
<label class="field-label" :class="{ 'field-modified': modifiedFields.email }">
|
||||||
<UIcon name="i-heroicons-envelope" class="w-4 h-4" />
|
<UIcon name="i-heroicons-envelope" class="w-4 h-4" />
|
||||||
Correo electrónico
|
Correo electrónico
|
||||||
<span class="required">*</span>
|
<span class="required">*</span>
|
||||||
|
<span v-if="modifiedFields.email" class="modified-indicator">
|
||||||
|
<UIcon name="i-heroicons-pencil-square" class="w-3 h-3" />
|
||||||
|
Modificado
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<UInput
|
<UInput
|
||||||
v-model="formData.email"
|
v-model="formData.email"
|
||||||
@@ -305,8 +313,15 @@ const isSubmitting = ref(false)
|
|||||||
const isUploading = ref(false)
|
const isUploading = ref(false)
|
||||||
const showCamera = ref(false)
|
const showCamera = ref(false)
|
||||||
|
|
||||||
// Datos del formulario
|
// Cookie para persistir cambios del formulario
|
||||||
const formData = ref({
|
const formCookie = useCookie<Record<string, string>>('profile-form-draft', {
|
||||||
|
maxAge: 60 * 60 * 24 * 7, // 7 días
|
||||||
|
sameSite: 'lax',
|
||||||
|
default: () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Datos originales del usuario (para comparar)
|
||||||
|
const originalData = ref({
|
||||||
name: user.value?.name || '',
|
name: user.value?.name || '',
|
||||||
email: user.value?.email || '',
|
email: user.value?.email || '',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
@@ -318,9 +333,62 @@ const formData = ref({
|
|||||||
nucleoCode: ''
|
nucleoCode: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Datos del formulario (restaurar desde cookie si existe)
|
||||||
|
const formData = ref({
|
||||||
|
name: formCookie.value.name || user.value?.name || '',
|
||||||
|
email: formCookie.value.email || user.value?.email || '',
|
||||||
|
avatar: formCookie.value.avatar || '',
|
||||||
|
phone: formCookie.value.phone || '',
|
||||||
|
cedula: formCookie.value.cedula || '',
|
||||||
|
birthdate: formCookie.value.birthdate || '',
|
||||||
|
nfc: formCookie.value.nfc || '',
|
||||||
|
pin: formCookie.value.pin || '',
|
||||||
|
nucleoCode: formCookie.value.nucleoCode || ''
|
||||||
|
})
|
||||||
|
|
||||||
// Avatar actual del usuario
|
// Avatar actual del usuario
|
||||||
const currentAvatar = ref(user.value?.avatar || '')
|
const currentAvatar = ref(user.value?.avatar || '')
|
||||||
|
|
||||||
|
// Detectar qué campos han sido modificados
|
||||||
|
const modifiedFields = computed(() => {
|
||||||
|
const modified: Record<string, boolean> = {}
|
||||||
|
const keys = Object.keys(formData.value) as Array<keyof typeof formData.value>
|
||||||
|
|
||||||
|
keys.forEach(key => {
|
||||||
|
modified[key] = formData.value[key] !== originalData.value[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
return modified
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verificar si hay cambios pendientes
|
||||||
|
const hasChanges = computed(() => {
|
||||||
|
return Object.values(modifiedFields.value).some(modified => modified)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Lista de campos modificados (para mostrar en alerta)
|
||||||
|
const modifiedFieldsList = computed(() => {
|
||||||
|
const fieldNames: Record<string, string> = {
|
||||||
|
name: 'Nombre completo',
|
||||||
|
email: 'Correo electrónico',
|
||||||
|
phone: 'Teléfono',
|
||||||
|
cedula: 'Cédula',
|
||||||
|
birthdate: 'Fecha de nacimiento',
|
||||||
|
nfc: 'NFC vinculada',
|
||||||
|
pin: 'PIN numérico',
|
||||||
|
nucleoCode: 'Código Nucleo V2'
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(modifiedFields.value)
|
||||||
|
.filter(([_, modified]) => modified)
|
||||||
|
.map(([field, _]) => fieldNames[field] || field)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Guardar en cookie cuando cambian los datos
|
||||||
|
watch(formData, (newData) => {
|
||||||
|
formCookie.value = { ...newData }
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
// Actualizar avatar cuando cambie el usuario
|
// Actualizar avatar cuando cambie el usuario
|
||||||
watch(() => user.value?.avatar, (newAvatar) => {
|
watch(() => user.value?.avatar, (newAvatar) => {
|
||||||
if (newAvatar) {
|
if (newAvatar) {
|
||||||
@@ -328,6 +396,36 @@ watch(() => user.value?.avatar, (newAvatar) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Alertar al usuario antes de salir si hay cambios
|
||||||
|
if (import.meta.client) {
|
||||||
|
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||||
|
if (hasChanges.value) {
|
||||||
|
const message = `Tienes cambios sin guardar en:\n${modifiedFieldsList.value.join(', ')}\n\nLos cambios quedarán guardados en memoria pero no subidos al servidor.`
|
||||||
|
e.preventDefault()
|
||||||
|
e.returnValue = message
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||||
|
|
||||||
|
// Mostrar notificación si hay datos restaurados
|
||||||
|
if (Object.keys(formCookie.value).length > 0 && hasChanges.value) {
|
||||||
|
toast.add({
|
||||||
|
title: 'Borrador restaurado',
|
||||||
|
description: `Se restauraron cambios pendientes en: ${modifiedFieldsList.value.join(', ')}`,
|
||||||
|
color: 'info',
|
||||||
|
icon: 'i-heroicons-document-text'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Enviar formulario
|
// Enviar formulario
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formData.value.name || !formData.value.email) {
|
if (!formData.value.name || !formData.value.email) {
|
||||||
@@ -351,6 +449,13 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Limpiar cookie de borrador al guardar exitosamente
|
||||||
|
formCookie.value = {}
|
||||||
|
|
||||||
|
// Actualizar datos originales
|
||||||
|
originalData.value.name = formData.value.name
|
||||||
|
originalData.value.email = formData.value.email
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'Perfil actualizado',
|
title: 'Perfil actualizado',
|
||||||
description: 'Tus cambios se guardaron correctamente. Recarga la página para verlos.',
|
description: 'Tus cambios se guardaron correctamente. Recarga la página para verlos.',
|
||||||
@@ -538,6 +643,11 @@ const removeAvatar = async () => {
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--color-gray-700);
|
color: var(--color-gray-700);
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-label.field-modified {
|
||||||
|
color: rgb(var(--color-warning-600));
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-label .required {
|
.field-label .required {
|
||||||
@@ -545,6 +655,20 @@ const removeAvatar = async () => {
|
|||||||
margin-left: 0.125rem;
|
margin-left: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modified-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
font-size: 0.6875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
background: rgba(var(--color-warning-500), 0.15);
|
||||||
|
color: rgb(var(--color-warning-600));
|
||||||
|
border: 1px solid rgba(var(--color-warning-500), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.field-help {
|
.field-help {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: var(--color-gray-500);
|
color: var(--color-gray-500);
|
||||||
@@ -639,6 +763,16 @@ const removeAvatar = async () => {
|
|||||||
color: var(--color-gray-300) !important;
|
color: var(--color-gray-300) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .field-label.field-modified {
|
||||||
|
color: rgb(var(--color-warning-400)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .modified-indicator {
|
||||||
|
background: rgba(var(--color-warning-500), 0.2) !important;
|
||||||
|
color: rgb(var(--color-warning-400)) !important;
|
||||||
|
border-color: rgba(var(--color-warning-500), 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.dark .field-help {
|
.dark .field-help {
|
||||||
color: var(--color-gray-400) !important;
|
color: var(--color-gray-400) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user