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 -->
|
||||
<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" />
|
||||
Nombre completo
|
||||
<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>
|
||||
<UInput
|
||||
v-model="formData.name"
|
||||
@@ -100,10 +104,14 @@
|
||||
|
||||
<!-- Email -->
|
||||
<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" />
|
||||
Correo electrónico
|
||||
<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>
|
||||
<UInput
|
||||
v-model="formData.email"
|
||||
@@ -305,8 +313,15 @@ const isSubmitting = ref(false)
|
||||
const isUploading = ref(false)
|
||||
const showCamera = ref(false)
|
||||
|
||||
// Datos del formulario
|
||||
const formData = ref({
|
||||
// Cookie para persistir cambios del formulario
|
||||
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 || '',
|
||||
email: user.value?.email || '',
|
||||
avatar: '',
|
||||
@@ -318,9 +333,62 @@ const formData = ref({
|
||||
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
|
||||
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
|
||||
watch(() => user.value?.avatar, (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
|
||||
const handleSubmit = async () => {
|
||||
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({
|
||||
title: 'Perfil actualizado',
|
||||
description: 'Tus cambios se guardaron correctamente. Recarga la página para verlos.',
|
||||
@@ -538,6 +643,11 @@ const removeAvatar = async () => {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-700);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.field-label.field-modified {
|
||||
color: rgb(var(--color-warning-600));
|
||||
}
|
||||
|
||||
.field-label .required {
|
||||
@@ -545,6 +655,20 @@ const removeAvatar = async () => {
|
||||
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 {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-gray-500);
|
||||
@@ -639,6 +763,16 @@ const removeAvatar = async () => {
|
||||
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 {
|
||||
color: var(--color-gray-400) !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user