Files
SIB/frontend/src/views/DriverDashboard.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>