575 lines
14 KiB
Vue
575 lines
14 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, onUnmounted, ref, computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useTaxiStore } from '@/stores/taxi'
|
|
import { analyticsService } from '@/services/analyticsService'
|
|
import type { Taxi } from '@/types'
|
|
import FavoriteButton from '@/components/FavoriteButton.vue'
|
|
import AppImage from '@/components/AppImage.vue'
|
|
import AuthGuard from '@/components/common/AuthGuard.vue'
|
|
import LoadingBranded from '@/components/common/LoadingBranded.vue'
|
|
|
|
const { t } = useI18n()
|
|
const taxiStore = useTaxiStore()
|
|
|
|
const selectedZone = ref('all')
|
|
const selectedShift = ref('all')
|
|
const onlyEnglish = ref(false)
|
|
|
|
const corregimientos = ['all', 'Boquete', 'David - Boquete', 'Boquete - David', 'Aeropuerto - Boquete']
|
|
const shifts = ['all', 'dia', 'tarde', 'noche']
|
|
|
|
function fetchData() {
|
|
taxiStore.loadTaxis()
|
|
}
|
|
|
|
function handleRefocus() {
|
|
// Recarga silenciosa: no congela la UI si ya hay datos
|
|
taxiStore.silentReload()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
analyticsService.logEvent({ event_name: 'screen_view', screen_name: 'TaxisLocales' })
|
|
window.addEventListener('app-refocus', handleRefocus)
|
|
if(taxiStore.taxis.length === 0) {
|
|
await fetchData()
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('app-refocus', handleRefocus)
|
|
})
|
|
|
|
const filteredTaxis = computed(() => {
|
|
return taxiStore.taxis.filter(taxi => {
|
|
const matchesZone = selectedZone.value === 'all' || taxi.corregimiento === selectedZone.value
|
|
// Ahora comprueba si el turno seleccionado está en el array de turnos del taxi
|
|
const matchesShift = selectedShift.value === 'all' || (taxi.shifts && taxi.shifts.includes(selectedShift.value))
|
|
const matchesEnglish = !onlyEnglish.value || taxi.english_speaking
|
|
return matchesZone && matchesShift && matchesEnglish
|
|
})
|
|
})
|
|
|
|
const isOnline = (taxi: Taxi) => {
|
|
if (!taxi.shifts) return false
|
|
return taxi.shifts.includes('dia') || taxi.shifts.includes('tarde')
|
|
}
|
|
|
|
const getShiftsDisplay = (taxi: Taxi) => {
|
|
if (!taxi.shifts || taxi.shifts.length === 0) return ''
|
|
return taxi.shifts.map(s => getShiftLabel(s)).join(' · ')
|
|
}
|
|
|
|
const handleCall = (taxi: Taxi) => {
|
|
analyticsService.logEvent({
|
|
event_name: 'taxi_click',
|
|
entity_type: 'taxi',
|
|
entity_id: taxi.id,
|
|
entity_name: taxi.owner_name,
|
|
properties: {
|
|
action: 'call',
|
|
taxi_id: taxi.id,
|
|
plate: taxi.license_plate
|
|
}
|
|
})
|
|
window.location.href = `tel:${taxi.phone_number}`
|
|
}
|
|
|
|
function getShiftLabel(shift: string) {
|
|
if (shift === 'dia') return t('taxi.dayShift')
|
|
if (shift === 'tarde') return t('taxi.afternoonShift')
|
|
if (shift === 'noche') return t('taxi.nightShift')
|
|
return shift
|
|
}
|
|
</script>
|
|
<template>
|
|
<div class="taxis-locales">
|
|
<div class="filters-container">
|
|
<div class="filter-card glass-effect">
|
|
<div class="selectors-side">
|
|
<div class="select-group-premium">
|
|
<div class="group-icon">
|
|
<span class="material-icons">location_on</span>
|
|
</div>
|
|
<div class="group-content">
|
|
<label>{{ t('taxi.allZones') }}</label>
|
|
<select v-model="selectedZone">
|
|
<option value="all">{{ t('taxi.allZones') }}</option>
|
|
<option v-for="zone in corregimientos.filter(z => z !== 'all')" :key="zone" :value="zone">{{ zone }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="select-group-premium">
|
|
<div class="group-icon">
|
|
<span class="material-icons">schedule</span>
|
|
</div>
|
|
<div class="group-content">
|
|
<label>{{ t('taxi.shift') }}</label>
|
|
<select v-model="selectedShift">
|
|
<option value="all">{{ t('taxi.shift') }}</option>
|
|
<option v-for="s in shifts.filter(x => x !== 'all')" :key="s" :value="s">{{ getShiftLabel(s) }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="lang-toggle-side">
|
|
<div class="lang-pill" :class="{ 'lang-pill--active': onlyEnglish }" @click="onlyEnglish = !onlyEnglish">
|
|
<span class="material-icons">{{ onlyEnglish ? 'check_circle' : 'language' }}</span>
|
|
<span>English</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="taxiStore.isLoading" class="state-container">
|
|
<LoadingBranded :message="t('taxi.loadingTaxis') || 'Cargando taxis...'" icon="local_taxi" />
|
|
</div>
|
|
|
|
<div v-else-if="taxiStore.error" class="state-container">
|
|
<span class="material-icons">error_outline</span>
|
|
<p>{{ taxiStore.error }}</p>
|
|
<button class="retry-btn" @click="taxiStore.loadTaxis()">
|
|
<span class="material-icons">refresh</span>
|
|
{{ t('common.retry') || 'Reintentar' }}
|
|
</button>
|
|
</div>
|
|
|
|
<AuthGuard
|
|
:title="t('discover.auth.title')"
|
|
:message="t('shuttle.auth.message')"
|
|
>
|
|
<div class="taxis-grid">
|
|
<div v-for="taxi in filteredTaxis" :key="taxi.id" v-memo="[taxi.id]" class="taxi-card-new glass-effect">
|
|
<div class="card-top">
|
|
<div class="driver-avatar-wrap">
|
|
<div class="driver-avatar">
|
|
<AppImage
|
|
:src="taxi.image_url"
|
|
type="taxi"
|
|
alt="Driver"
|
|
/>
|
|
</div>
|
|
<div class="driver-status" :class="{ 'status-online': isOnline(taxi) }"></div>
|
|
</div>
|
|
<div class="driver-info">
|
|
<div class="flex items-center gap-2 mb-0.5">
|
|
<h3 class="driver-name">{{ taxi.owner_name }}</h3>
|
|
<span v-if="taxi.is_accessible" class="material-icons text-blue-500 text-sm" title="Accesible para personas con discapacidad">accessible</span>
|
|
</div>
|
|
<div class="driver-meta">
|
|
<div class="rating-stars">
|
|
<span class="material-icons star-filled">star</span>
|
|
<span class="rating-value">{{ (taxi.rating || 5).toFixed(1) }}</span>
|
|
</div>
|
|
<span class="meta-dot">·</span>
|
|
<span class="shift-badge">{{ getShiftsDisplay(taxi) }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="fav-icon-wrapper">
|
|
<FavoriteButton
|
|
item-type="taxi"
|
|
:item-id="taxi.id"
|
|
:item-name="taxi.owner_name"
|
|
:item-image="taxi.image_url || undefined"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-details">
|
|
<div class="detail-item" v-if="taxi.corregimiento">
|
|
<span class="material-icons detail-icon">location_on</span>
|
|
<span class="detail-text">{{ taxi.corregimiento }}</span>
|
|
</div>
|
|
<div class="detail-item" v-if="taxi.vehicle_type">
|
|
<span class="material-icons detail-icon">local_taxi</span>
|
|
<span class="detail-text">{{ taxi.vehicle_type }}</span>
|
|
</div>
|
|
<div class="detail-item" v-if="taxi.english_speaking">
|
|
<span class="material-icons detail-icon">g_translate</span>
|
|
<span class="detail-text">{{ t('taxi.englishLabel') }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-actions">
|
|
<a :href="`tel:${taxi.phone_number}`" class="call-btn-premium" @click="handleCall(taxi)">
|
|
<span class="material-icons">phone_in_talk</span>
|
|
<div class="btn-content">
|
|
<span class="btn-label">{{ t('taxi.callNow') }}</span>
|
|
<span class="btn-subtext">{{ taxi.phone_number }}</span>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="filteredTaxis.length === 0" class="empty-state">
|
|
<span class="material-icons">no_accounts</span>
|
|
<p>{{ t('taxi.noTaxisAvailable') }}</p>
|
|
</div>
|
|
</div>
|
|
</AuthGuard>
|
|
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* FILTROS PREMIUM */
|
|
.filters-container {
|
|
padding: 0 1rem 1.5rem;
|
|
}
|
|
|
|
.filter-card {
|
|
border-radius: 1.5rem;
|
|
padding: 1.25rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.25rem;
|
|
border: 1px solid var(--border-color);
|
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
|
background: var(--card-bg);
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.filter-card {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
}
|
|
|
|
.selectors-side {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 1rem;
|
|
}
|
|
|
|
@media (min-width: 480px) {
|
|
.selectors-side {
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
}
|
|
|
|
.select-group-premium {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.875rem;
|
|
background: var(--bg-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 1rem;
|
|
padding: 0.625rem 0.875rem;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.select-group-premium:focus-within {
|
|
border-color: var(--active-color);
|
|
box-shadow: 0 0 0 3px rgba(254, 231, 21, 0.1);
|
|
}
|
|
|
|
.group-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 36px;
|
|
height: 36px;
|
|
background: var(--bg-secondary);
|
|
border-radius: 0.75rem;
|
|
color: var(--active-color);
|
|
}
|
|
|
|
.group-content {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.group-content label {
|
|
font-size: 0.65rem;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-secondary);
|
|
line-height: 1;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.group-content select {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-primary);
|
|
font-weight: 700;
|
|
font-size: 0.9375rem;
|
|
outline: none;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
width: 100%;
|
|
}
|
|
|
|
.lang-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.625rem 1.25rem;
|
|
background: var(--bg-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 1rem;
|
|
font-weight: 700;
|
|
font-size: 0.875rem;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.lang-pill--active {
|
|
background: rgba(254, 231, 21, 0.1);
|
|
border-color: var(--active-color);
|
|
color: var(--active-color);
|
|
}
|
|
|
|
/* GRID Y TARJETAS PREMIUM */
|
|
.taxis-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 1.5rem;
|
|
padding: 1.5rem 1rem;
|
|
}
|
|
|
|
.taxi-card-new {
|
|
border-radius: 1.5rem;
|
|
padding: 1.25rem;
|
|
border: 1px solid var(--border-color);
|
|
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.25rem;
|
|
background: var(--card-bg);
|
|
}
|
|
|
|
.taxi-card-new:hover {
|
|
transform: translateY(-8px);
|
|
border-color: var(--active-color);
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.card-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.driver-avatar-wrap {
|
|
position: relative;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.driver-avatar {
|
|
width: 56px;
|
|
height: 56px;
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
background: var(--bg-secondary);
|
|
border: 2px solid var(--border-color);
|
|
}
|
|
|
|
.driver-avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.driver-status {
|
|
position: absolute;
|
|
bottom: -2px;
|
|
right: -2px;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
background: #94a3b8;
|
|
border: 3px solid var(--card-bg);
|
|
}
|
|
|
|
.status-online {
|
|
background: #22c55e;
|
|
}
|
|
|
|
.driver-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.driver-name {
|
|
margin: 0 0 0.25rem;
|
|
font-size: 1.125rem;
|
|
font-weight: 800;
|
|
color: var(--text-primary);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.driver-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.rating-stars {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.star-filled {
|
|
color: var(--active-color);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.rating-value {
|
|
font-size: 0.875rem;
|
|
font-weight: 800;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.meta-dot {
|
|
color: var(--text-secondary);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.shift-badge {
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
color: var(--text-secondary);
|
|
text-transform: capitalize;
|
|
}
|
|
|
|
.card-details {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.detail-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.375rem;
|
|
background: var(--bg-secondary);
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 0.75rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.detail-icon {
|
|
font-size: 1rem;
|
|
color: var(--active-color);
|
|
}
|
|
|
|
.card-actions {
|
|
margin-top: auto;
|
|
}
|
|
|
|
.call-btn-premium {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
width: 100%;
|
|
padding: 0.75rem 1.25rem;
|
|
background: var(--active-color);
|
|
color: #101820;
|
|
border-radius: 1.125rem;
|
|
text-decoration: none;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 4px 15px rgba(254, 231, 21, 0.2);
|
|
}
|
|
|
|
.call-btn-premium:hover {
|
|
transform: scale(1.02);
|
|
box-shadow: 0 6px 20px rgba(254, 231, 21, 0.35);
|
|
}
|
|
|
|
.call-btn-premium:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.call-btn-premium .material-icons {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.btn-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.btn-label {
|
|
font-size: 0.9375rem;
|
|
font-weight: 900;
|
|
line-height: 1;
|
|
}
|
|
|
|
.btn-subtext {
|
|
font-size: 0.75rem;
|
|
font-weight: 700;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.empty-state {
|
|
grid-column-start: 1;
|
|
grid-column-end: -1;
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state .material-icons {
|
|
font-size: 4rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.empty-state p {
|
|
font-size: 1.125rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.state-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 4rem 2rem;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.spin {
|
|
animation: spin 1s infinite linear;
|
|
font-size: 3rem;
|
|
color: var(--active-color);
|
|
}
|
|
|
|
@keyframes spin {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.retry-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.75rem 1.5rem;
|
|
background: var(--active-color);
|
|
color: #101820;
|
|
border: none;
|
|
border-radius: 99px;
|
|
font-weight: 800;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.retry-btn:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
.taxis-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style> |