Implement Smart Location: auto-detect user location if preference is enabled, hide location button, and handle permission denial by resetting preference

This commit is contained in:
2026-03-01 12:15:08 -05:00
parent d0d75b8c98
commit 4d7b472c6c
20 changed files with 852 additions and 344 deletions

View File

@ -4,11 +4,13 @@ import { useRouter } from 'vue-router'
import { useCouponStore } from '@/stores/coupon'
import { authService } from '@/services/authService'
import { getImageUrl } from '@/utils/imageUrl'
import { useI18n } from 'vue-i18n'
const router = useRouter()
const { t } = useI18n()
const couponStore = useCouponStore()
const userName = ref(localStorage.getItem('user_name') || 'Usuario')
const userName = ref(localStorage.getItem('user_name') || t('profile.user'))
const userEmail = ref(localStorage.getItem('user_email') || '')
const userRole = ref(localStorage.getItem('user_role') || 'PASSENGER')
const userPhoto = ref(localStorage.getItem('profile_photo_url') || '')
@ -89,9 +91,9 @@ async function handleUpdateProfile() {
showEditModal.value = false
editForm.value.password = ''
alert('Perfil actualizado correctamente')
alert(t('profile.updateSuccess'))
} catch (e: any) {
alert('Error al actualizar: ' + (e.response?.data?.detail || e.message))
alert(t('profile.updateError') + ' ' + (e.response?.data?.detail || e.message))
} finally {
isUpdating.value = false
}
@ -99,9 +101,9 @@ async function handleUpdateProfile() {
function getStatusLabel(status: string) {
switch (status) {
case 'claimed': return 'Pendiente'
case 'redeemed': return 'Canjeado'
case 'expired': return 'Vencido'
case 'claimed': return t('profile.pending')
case 'redeemed': return t('profile.redeemed')
case 'expired': return t('profile.expired')
default: return status
}
}
@ -132,7 +134,7 @@ const getFullUrl = (path: string) => getImageUrl(path)
</div>
<div class="header-actions">
<button class="logout-icon-btn" @click="handleLogout" title="Cerrar Sesión">
<button class="logout-icon-btn" @click="handleLogout" :title="t('profile.logoutTitle')">
<span class="material-icons">logout</span>
</button>
</div>
@ -141,7 +143,7 @@ const getFullUrl = (path: string) => getImageUrl(path)
<section class="my-coupons-section">
<div class="section-header">
<h2>Mis Cupones</h2>
<h2>{{ t('profile.myCoupons') }}</h2>
<span class="count">{{ couponStore.myCoupons.length }}</span>
</div>
@ -149,9 +151,9 @@ const getFullUrl = (path: string) => getImageUrl(path)
<div class="empty-icon-circle">
<span class="material-icons">confirmation_number</span>
</div>
<h3>No tienes cupones</h3>
<p>Explora los beneficios que tenemos para ti por usar SIBU.</p>
<button @click="router.push('/coupons')" class="btn-primary">Ver Ofertas</button>
<h3>{{ t('profile.emptyCoupons') }}</h3>
<p>{{ t('profile.exploreOffers') }}</p>
<button @click="router.push('/coupons')" class="btn-primary">{{ t('profile.viewOffers') }}</button>
</div>
<div v-else class="coupons-list">
@ -162,8 +164,8 @@ const getFullUrl = (path: string) => getImageUrl(path)
>
<div class="coupon-main">
<div class="coupon-details">
<h3>{{ userCoupon.coupon?.title || 'Cupón' }}</h3>
<p class="biz-name">{{ userCoupon.coupon?.business?.name || 'Comercio' }}</p>
<h3>{{ userCoupon.coupon?.title || t('coupons.title') }}</h3> // Reusing coupons.title for fallback
<p class="biz-name">{{ userCoupon.coupon?.business?.name || t('coupons.restaurant') }}</p>
<div class="code-row">
<span class="code">{{ userCoupon.redemption_code }}</span>
<span :class="['status-tag', userCoupon.status]">{{ getStatusLabel(userCoupon.status) }}</span>
@ -175,12 +177,12 @@ const getFullUrl = (path: string) => getImageUrl(path)
@click="showQR(userCoupon.redemption_code, userCoupon.coupon?.title || '')"
>
<span class="material-icons">qr_code_2</span>
Ver Código
{{ t('profile.viewCode') }}
</button>
</div>
<div class="coupon-footer">
<span v-if="userCoupon.status === 'redeemed'">Usado el: {{ new Date(userCoupon.redeemed_at).toLocaleDateString() }}</span>
<span v-else>Reclamado el: {{ new Date(userCoupon.claimed_at).toLocaleDateString() }}</span>
<span v-if="userCoupon.status === 'redeemed'">{{ t('profile.usedAt') }} {{ new Date(userCoupon.redeemed_at).toLocaleDateString() }}</span>
<span v-else>{{ t('profile.claimedAt') }} {{ new Date(userCoupon.claimed_at).toLocaleDateString() }}</span>
</div>
</div>
</div>
@ -190,7 +192,7 @@ const getFullUrl = (path: string) => getImageUrl(path)
<div v-if="showEditModal" class="modal-overlay" @click.self="showEditModal = false">
<div class="edit-modal">
<div class="modal-header">
<h2>Editar Perfil</h2>
<h2>{{ t('profile.editProfile') }}</h2>
<button class="close-btn" @click="showEditModal = false">
<span class="material-icons">close</span>
</button>
@ -208,25 +210,25 @@ const getFullUrl = (path: string) => getImageUrl(path)
</label>
</div>
<input id="photo-input" type="file" @change="handlePhotoChange" accept="image/*" hidden>
<p class="upload-hint">Foto opcional</p>
<p class="upload-hint">{{ t('profile.photoOptional') }}</p>
</div>
<div class="form-group">
<label>Nombre Completo</label>
<input v-model="editForm.full_name" type="text" placeholder="Tu nombre" required>
<label>{{ t('profile.nameLabel') }}</label>
<input v-model="editForm.full_name" type="text" :placeholder="t('profile.namePlaceholder')" required>
</div>
<div class="form-group">
<label>Nueva Contraseña (Opcional)</label>
<input v-model="editForm.password" type="password" placeholder="Mínimo 6 caracteres">
<p class="field-hint">Déjalo en blanco si no quieres cambiarla.</p>
<label>{{ t('profile.passwordOptional') }}</label>
<input v-model="editForm.password" type="password" :placeholder="t('profile.passwordPlaceholder')">
<p class="field-hint">{{ t('profile.passwordHint') }}</p>
</div>
<div class="modal-actions">
<button type="button" class="btn-cancel" @click="showEditModal = false">Cancelar</button>
<button type="button" class="btn-cancel" @click="showEditModal = false">{{ t('profile.cancel') }}</button>
<button type="submit" class="btn-save" :disabled="isUpdating">
<span v-if="isUpdating" class="material-icons spin">refresh</span>
{{ isUpdating ? 'Guardando...' : 'Guardar Cambios' }}
{{ isUpdating ? t('profile.saving') : t('profile.save') }}
</button>
</div>
</form>
@ -241,7 +243,7 @@ const getFullUrl = (path: string) => getImageUrl(path)
</button>
<div class="qr-header">
<span class="material-icons">verified</span>
<h3>Cupón de Descuento</h3>
<h3>{{ t('profile.qrTitle') }}</h3>
</div>
<p class="promo-title">{{ selectedTitle }}</p>
@ -250,14 +252,14 @@ const getFullUrl = (path: string) => getImageUrl(path)
<span class="material-icons">qr_code_2</span>
</div>
<div class="redemption-box">
<p>CÓDIGO DE REDENCIÓN</p>
<p>{{ t('profile.qrCode') }}</p>
<code class="big-code">{{ selectedCode }}</code>
</div>
</div>
<p class="qr-instructions">Muestra este código al encargado del establecimiento para validar tu promoción.</p>
<p class="qr-instructions">{{ t('profile.qrInstructions') }}</p>
<button class="btn-done" @click="showQRModal = false">Entendido</button>
<button class="btn-done" @click="showQRModal = false">{{ t('profile.understood') }}</button>
</div>
</div>
</div>
@ -634,6 +636,13 @@ const getFullUrl = (path: string) => getImageUrl(path)
cursor: pointer;
}
/* Modal Actions */
.modal-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
/* QR Modal Enhanced */
.qr-modal {
background: var(--card-bg);