Files
SIB/frontend/src/views/transporte/TaxisLocales.vue

574 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 { getImageUrl } from '@/utils/imageUrl'
import AuthGuard from '@/components/common/AuthGuard.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() {
fetchData()
}
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',
item_id: 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">
<span class="material-icons spin">refresh</span>
<p>{{ t('taxi.loadingTaxis') }}</p>
</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">
<img
:src="getImageUrl(taxi.image_url, 'taxi')"
loading="lazy"
decoding="async"
alt="Driver"
@error="(e) => (e.target as HTMLImageElement).src = getImageUrl(null, 'taxi')"
>
</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>