Initial commit: SIBU 2.0 MISSION
This commit is contained in:
403
frontend/src/views/DriverDashboard.vue
Normal file
403
frontend/src/views/DriverDashboard.vue
Normal file
@ -0,0 +1,403 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user