404 lines
10 KiB
Vue
404 lines
10 KiB
Vue
<template>
|
|
<div class="driver-dashboard">
|
|
<div class="dashboard-header">
|
|
<div class="user-welcome">
|
|
<h1>Hola, {{ userName }}</h1>
|
|
<p>Panel de Control de Transportista</p>
|
|
</div>
|
|
<button class="logout-btn" @click="handleLogout">Cerrar Sesión</button>
|
|
</div>
|
|
|
|
<!-- Verification Status Banner -->
|
|
<div v-if="!isVerified" class="status-banner pending">
|
|
<span class="material-icons">hourglass_empty</span>
|
|
<div class="banner-text">
|
|
<h3>Tu cuenta está pendiente de verificación</h3>
|
|
<p>Un administrador revisará tus documentos pronto. Mientras tanto, algunas funciones están limitadas.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="status-banner verified">
|
|
<span class="material-icons">verified</span>
|
|
<div class="banner-text">
|
|
<h3>Cuenta Verificada</h3>
|
|
<p>¡Felicidades! Tu cuenta está activa y puedes operar en el sistema.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-grid">
|
|
<!-- Profile Status Card -->
|
|
<div class="dashboard-card">
|
|
<h3>Mi Perfil</h3>
|
|
<div class="profile-preview">
|
|
<div class="info-row">
|
|
<label>Cédula:</label>
|
|
<span>{{ driverProfile?.cedula || '---' }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<label>Vehículo:</label>
|
|
<span>{{ (driverProfile?.vehicle_type || '---').toUpperCase() }}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<label>Placa:</label>
|
|
<span>{{ driverProfile?.license_plate || '---' }}</span>
|
|
</div>
|
|
</div>
|
|
<button class="secondary-btn" @click="editProfile" :disabled="!isVerified">Editar Perfil</button>
|
|
</div>
|
|
|
|
<!-- Real-time Availability Card -->
|
|
<div class="dashboard-card" :class="{ 'active-service': isInService }">
|
|
<div class="card-header-flex">
|
|
<h3>Estado en Tiempo Real</h3>
|
|
<div class="status-indicator" :class="{ 'online': isInService }"></div>
|
|
</div>
|
|
|
|
<div class="service-controls">
|
|
<p v-if="!isInService" class="service-hint">Activa tu ubicación para que los pasajeros puedan verte en el mapa.</p>
|
|
<p v-else class="service-hint active">Tu ubicación se está transmitiendo en tiempo real.</p>
|
|
|
|
<button
|
|
@click="toggleService"
|
|
:class="isInService ? 'service-btn stop' : 'service-btn start'"
|
|
:disabled="!isVerified || isLocating"
|
|
>
|
|
<span class="material-icons">{{ isInService ? 'location_off' : 'location_on' }}</span>
|
|
{{ isInService ? 'Detener Servicio' : 'Iniciar Servicio' }}
|
|
</button>
|
|
|
|
<div v-if="isLocating" class="locating-spinner">
|
|
<span class="material-icons spin">refresh</span>
|
|
Obteniendo ubicación...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { authService } from '@/services/authService'
|
|
import { telemetryService } from '@/services/telemetryService'
|
|
|
|
const router = useRouter()
|
|
const userName = localStorage.getItem('user_name') || 'Conductor'
|
|
const isVerified = ref(false)
|
|
const driverProfile = ref<any>(null)
|
|
|
|
// Service tracking state
|
|
const isInService = ref(false)
|
|
const isLocating = ref(false)
|
|
const watchId = ref<number | null>(null)
|
|
const lastUpdate = ref<number>(0)
|
|
const minUpdateInterval = 10000 // 10 seconds
|
|
|
|
onMounted(async () => {
|
|
await fetchStatus()
|
|
|
|
// Restore state if was in service (optional, simpler for now to start off)
|
|
const savedService = localStorage.getItem('driver_in_service')
|
|
if (savedService === 'true' && isVerified.value) {
|
|
// startService() // Consider if auto-start is safe
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
stopService()
|
|
})
|
|
|
|
async function fetchStatus() {
|
|
try {
|
|
const user = await authService.getCurrentUser()
|
|
isVerified.value = user.is_verified
|
|
driverProfile.value = user.driver_profile
|
|
|
|
// Update local storage just in case
|
|
localStorage.setItem('user_verified', user.is_verified.toString())
|
|
} catch (e) {
|
|
console.error('Failed to fetch driver status', e)
|
|
}
|
|
}
|
|
|
|
function handleLogout() {
|
|
localStorage.clear()
|
|
router.push('/login')
|
|
}
|
|
|
|
async function toggleService() {
|
|
if (isInService.value) {
|
|
stopService()
|
|
} else {
|
|
await startService()
|
|
}
|
|
}
|
|
|
|
async function startService() {
|
|
if (!navigator.geolocation) {
|
|
alert('Tu navegador no soporta geolocalización')
|
|
return
|
|
}
|
|
|
|
isLocating.value = true
|
|
isInService.value = true
|
|
localStorage.setItem('driver_in_service', 'true')
|
|
|
|
watchId.value = navigator.geolocation.watchPosition(
|
|
async (position) => {
|
|
isLocating.value = false
|
|
const now = Date.now()
|
|
|
|
// Throttling updates to save battery/bandwidth
|
|
if (now - lastUpdate.value >= minUpdateInterval) {
|
|
try {
|
|
await telemetryService.sendTelemetry({
|
|
latitude: position.coords.latitude,
|
|
longitude: position.coords.longitude,
|
|
speed: position.coords.speed || undefined,
|
|
heading: position.coords.heading || undefined,
|
|
status: 'active'
|
|
})
|
|
lastUpdate.value = now
|
|
} catch (e) {
|
|
console.error('Failed to send telemetry', e)
|
|
}
|
|
}
|
|
},
|
|
(error) => {
|
|
console.error('Geolocation error', error)
|
|
isLocating.value = false
|
|
if (error.code === error.PERMISSION_DENIED) {
|
|
alert('Debes permitir el acceso a la ubicación para usar esta función')
|
|
stopService()
|
|
}
|
|
},
|
|
{
|
|
enableHighAccuracy: true,
|
|
maximumAge: 30000,
|
|
timeout: 27000
|
|
}
|
|
)
|
|
}
|
|
|
|
function stopService() {
|
|
if (watchId.value !== null) {
|
|
navigator.geolocation.clearWatch(watchId.value)
|
|
watchId.value = null
|
|
}
|
|
|
|
isInService.value = false
|
|
isLocating.value = false
|
|
localStorage.setItem('driver_in_service', 'false')
|
|
|
|
// Optionally notify backend that we are offline
|
|
telemetryService.sendTelemetry({
|
|
latitude: 0,
|
|
longitude: 0,
|
|
status: 'offline'
|
|
}).catch(() => {})
|
|
}
|
|
|
|
function editProfile() {
|
|
alert('Función de edición de perfil próximamente')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.driver-dashboard {
|
|
padding: 24px;
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.dashboard-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.user-welcome h1 { margin: 0; font-size: 28px; }
|
|
.user-welcome p { color: #666; margin: 4px 0 0 0; }
|
|
|
|
.logout-btn {
|
|
background: transparent;
|
|
border: 1px solid #ddd;
|
|
padding: 8px 16px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.status-banner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 24px;
|
|
border-radius: 12px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.status-banner.pending {
|
|
background-color: #fff8e1;
|
|
color: #856404;
|
|
border: 1px solid #ffeeba;
|
|
}
|
|
|
|
.status-banner.verified {
|
|
background-color: #e8f5e9;
|
|
color: #2e7d32;
|
|
border: 1px solid #c8e6c9;
|
|
}
|
|
|
|
.banner-text h3 { margin: 0; font-size: 18px; }
|
|
.banner-text p { margin: 4px 0 0 0; font-size: 14px; opacity: 0.9; }
|
|
|
|
.status-banner .material-icons { font-size: 48px; }
|
|
|
|
.dashboard-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 24px;
|
|
}
|
|
|
|
.dashboard-card {
|
|
background: var(--card-bg);
|
|
padding: 24px;
|
|
border-radius: 16px;
|
|
box-shadow: 0 4px 12px var(--shadow);
|
|
display: flex;
|
|
flex-direction: column;
|
|
border: 1px solid var(--border-color);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.dashboard-card.active-service {
|
|
border-color: var(--active-color);
|
|
box-shadow: 0 0 15px rgba(25, 118, 210, 0.2);
|
|
}
|
|
|
|
.dashboard-card h3 { margin: 0 0 20px 0; font-size: 1.25rem; color: var(--text-primary); }
|
|
|
|
.card-header-flex {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
.card-header-flex h3 { margin: 0; }
|
|
|
|
.status-indicator {
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: #ccc;
|
|
}
|
|
.status-indicator.online {
|
|
background: #4caf50;
|
|
box-shadow: 0 0 10px #4caf50;
|
|
animation: blink 2s infinite;
|
|
}
|
|
|
|
@keyframes blink {
|
|
0% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
.info-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.info-row label { color: var(--text-secondary); }
|
|
.info-row span { font-weight: 600; color: var(--text-primary); }
|
|
|
|
.secondary-btn {
|
|
margin-top: auto;
|
|
padding: 12px;
|
|
background: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.secondary-btn:hover:not(:disabled) { background: var(--hover-bg); }
|
|
.secondary-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
.service-controls {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
height: 100%;
|
|
justify-content: center;
|
|
}
|
|
|
|
.service-hint {
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
text-align: center;
|
|
}
|
|
.service-hint.active {
|
|
color: var(--active-color);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.service-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
padding: 16px;
|
|
border-radius: 12px;
|
|
border: none;
|
|
font-weight: 800;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.service-btn.start {
|
|
background: var(--text-primary);
|
|
color: var(--bg-primary);
|
|
}
|
|
.service-btn.stop {
|
|
background: #ef5350;
|
|
color: white;
|
|
}
|
|
|
|
.locating-spinner {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.spin {
|
|
animation: spin 1s linear infinite;
|
|
font-size: 16px;
|
|
}
|
|
|
|
@keyframes spin { 100% { transform: rotate(360deg); } }
|
|
|
|
.unavailable { opacity: 0.7; }
|
|
|
|
.coming-soon {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
padding: 20px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.coming-soon .material-icons { font-size: 48px; margin-bottom: 8px; }
|
|
</style>
|