Documentation Index
Fetch the complete documentation index at: https://docs.bookovia.com/llms.txt
Use this file to discover all available pages before exploring further.
Web Dashboard Guide
Create powerful web-based fleet management dashboards with real-time vehicle tracking, comprehensive analytics, and interactive reporting using the Bookovia Telematics API and modern web frameworks.Dashboard Architecture
Component Overview
A comprehensive fleet dashboard typically includes:- Real-time Map View - Live vehicle positions and routes
- Fleet Overview - High-level metrics and KPIs
- Trip Management - Active and completed trip monitoring
- Safety Analytics - Driver behavior and incident tracking
- Reporting System - Historical data and custom reports
- Alert Management - Real-time notifications and alerts
Technology Stack
| Framework | Use Case | Strengths |
|---|---|---|
| React | Modern SPA dashboards | Component ecosystem, real-time updates |
| Vue.js | Rapid prototyping | Gentle learning curve, excellent tooling |
| Angular | Enterprise applications | TypeScript, comprehensive framework |
| Svelte | Performance-critical dashboards | Small bundle size, reactive updates |
| Next.js | Full-stack applications | SSR, API routes, deployment |
Getting Started
Project Setup
- React
- Vue.js
- Next.js
# Create React app
npx create-react-app bookovia-dashboard
cd bookovia-dashboard
# Install dependencies
npm install @bookovia/javascript-sdk
npm install react-router-dom
npm install react-leaflet leaflet
npm install recharts
npm install @headlessui/react @heroicons/react
npm install tailwindcss
# Start development
npm start
src/
├── components/
│ ├── Map/
│ │ ├── FleetMap.jsx
│ │ └── VehicleMarker.jsx
│ ├── Dashboard/
│ │ ├── Overview.jsx
│ │ ├── Metrics.jsx
│ │ └── LiveFeed.jsx
│ └── Reports/
│ ├── SafetyReport.jsx
│ └── TripReport.jsx
├── hooks/
│ ├── useBookovia.js
│ ├── useRealtime.js
│ └── useFleetData.js
├── services/
│ ├── api.js
│ └── websocket.js
└── pages/
├── Dashboard.jsx
├── Fleet.jsx
└── Reports.jsx
# Create Vue app
npm create vue@latest bookovia-dashboard
cd bookovia-dashboard
# Install dependencies
npm install @bookovia/javascript-sdk
npm install vue-router
npm install vue-leaflet
npm install vue-chartjs chart.js
npm install @headlessui/vue @heroicons/vue
npm install tailwindcss
# Start development
npm run dev
src/
├── components/
│ ├── Map/
│ │ ├── FleetMap.vue
│ │ └── VehicleMarker.vue
│ ├── Dashboard/
│ │ ├── Overview.vue
│ │ ├── Metrics.vue
│ │ └── LiveFeed.vue
│ └── Reports/
│ ├── SafetyReport.vue
│ └── TripReport.vue
├── composables/
│ ├── useBookovia.js
│ ├── useRealtime.js
│ └── useFleetData.js
├── services/
│ ├── api.js
│ └── websocket.js
└── views/
├── Dashboard.vue
├── Fleet.vue
└── Reports.vue
# Create Next.js app
npx create-next-app@latest bookovia-dashboard --typescript --tailwind --eslint
cd bookovia-dashboard
# Install dependencies
npm install @bookovia/javascript-sdk
npm install leaflet react-leaflet
npm install recharts
npm install @headlessui/react @heroicons/react
# Start development
npm run dev
src/
├── app/
│ ├── dashboard/
│ │ └── page.tsx
│ ├── fleet/
│ │ └── page.tsx
│ └── api/
│ └── bookovia/
│ └── route.ts
├── components/
│ ├── Map/
│ ├── Dashboard/
│ └── Reports/
├── hooks/
│ └── useBookovia.ts
├── lib/
│ ├── bookovia.ts
│ └── websocket.ts
└── types/
└── bookovia.ts
SDK Integration
- React
- Vue.js
- Next.js
// hooks/useBookovia.js
import { useState, useEffect, useCallback } from 'react';
import Bookovia from '@bookovia/javascript-sdk';
const useBookovia = () => {
const [client, setClient] = useState(null);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const initializeClient = async () => {
try {
const bookoviaClient = new Bookovia({
apiKey: process.env.REACT_APP_BOOKOVIA_API_KEY,
environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox'
});
await bookoviaClient.initialize();
setClient(bookoviaClient);
setIsConnected(true);
} catch (err) {
setError(err.message);
console.error('Bookovia initialization failed:', err);
}
};
initializeClient();
}, []);
const apiCall = useCallback(async (method, ...args) => {
if (!client) throw new Error('Bookovia client not initialized');
try {
return await client[method](...args);
} catch (err) {
setError(err.message);
throw err;
}
}, [client]);
return { client, isConnected, error, apiCall };
};
export default useBookovia;
// hooks/useRealtime.js
import { useState, useEffect, useRef } from 'react';
import useBookovia from './useBookovia';
const useRealtime = (channels = ['locations', 'safety_events']) => {
const { client } = useBookovia();
const [data, setData] = useState({});
const [connectionStatus, setConnectionStatus] = useState('disconnected');
const wsRef = useRef(null);
useEffect(() => {
if (!client) return;
const connectWebSocket = async () => {
try {
wsRef.current = await client.streaming.connect();
wsRef.current.onOpen(() => {
setConnectionStatus('connected');
// Subscribe to channels
channels.forEach(channel => {
wsRef.current.subscribe(channel);
});
});
wsRef.current.onMessage((message) => {
setData(prevData => ({
...prevData,
[message.type]: message.data
}));
});
wsRef.current.onClose(() => {
setConnectionStatus('disconnected');
// Auto-reconnect after 5 seconds
setTimeout(connectWebSocket, 5000);
});
} catch (error) {
console.error('WebSocket connection failed:', error);
setConnectionStatus('error');
}
};
connectWebSocket();
return () => {
if (wsRef.current) {
wsRef.current.disconnect();
}
};
}, [client, channels]);
return { data, connectionStatus };
};
export default useRealtime;
// composables/useBookovia.js
import { ref, onMounted } from 'vue';
import Bookovia from '@bookovia/javascript-sdk';
export const useBookovia = () => {
const client = ref(null);
const isConnected = ref(false);
const error = ref(null);
const initializeClient = async () => {
try {
const bookoviaClient = new Bookovia({
apiKey: import.meta.env.VITE_BOOKOVIA_API_KEY,
environment: import.meta.env.MODE === 'production' ? 'production' : 'sandbox'
});
await bookoviaClient.initialize();
client.value = bookoviaClient;
isConnected.value = true;
} catch (err) {
error.value = err.message;
console.error('Bookovia initialization failed:', err);
}
};
const apiCall = async (method, ...args) => {
if (!client.value) throw new Error('Bookovia client not initialized');
try {
return await client.value[method](...args);
} catch (err) {
error.value = err.message;
throw err;
}
};
onMounted(() => {
initializeClient();
});
return { client, isConnected, error, apiCall };
};
// composables/useRealtime.js
import { ref, onMounted, onUnmounted, watch } from 'vue';
import { useBookovia } from './useBookovia';
export const useRealtime = (channels = ['locations', 'safety_events']) => {
const { client } = useBookovia();
const data = ref({});
const connectionStatus = ref('disconnected');
let ws = null;
const connectWebSocket = async () => {
if (!client.value) return;
try {
ws = await client.value.streaming.connect();
ws.onOpen(() => {
connectionStatus.value = 'connected';
channels.forEach(channel => {
ws.subscribe(channel);
});
});
ws.onMessage((message) => {
data.value = {
...data.value,
[message.type]: message.data
};
});
ws.onClose(() => {
connectionStatus.value = 'disconnected';
setTimeout(connectWebSocket, 5000);
});
} catch (error) {
console.error('WebSocket connection failed:', error);
connectionStatus.value = 'error';
}
};
watch(client, (newClient) => {
if (newClient) {
connectWebSocket();
}
}, { immediate: true });
onUnmounted(() => {
if (ws) {
ws.disconnect();
}
});
return { data, connectionStatus };
};
// lib/bookovia.ts
import Bookovia from '@bookovia/javascript-sdk';
let bookoviaClient: Bookovia | null = null;
export const getBookoviaClient = async (): Promise<Bookovia> => {
if (bookoviaClient) return bookoviaClient;
bookoviaClient = new Bookovia({
apiKey: process.env.NEXT_PUBLIC_BOOKOVIA_API_KEY!,
environment: process.env.NODE_ENV === 'production' ? 'production' : 'sandbox'
});
await bookoviaClient.initialize();
return bookoviaClient;
};
// hooks/useBookovia.ts
import { useState, useEffect, useCallback } from 'react';
import { getBookoviaClient } from '../lib/bookovia';
import type Bookovia from '@bookovia/javascript-sdk';
export const useBookovia = () => {
const [client, setClient] = useState<Bookovia | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const initializeClient = async () => {
try {
const bookoviaClient = await getBookoviaClient();
setClient(bookoviaClient);
setIsConnected(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
};
initializeClient();
}, []);
const apiCall = useCallback(async (method: string, ...args: any[]) => {
if (!client) throw new Error('Bookovia client not initialized');
try {
return await (client as any)[method](...args);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
setError(errorMessage);
throw err;
}
}, [client]);
return { client, isConnected, error, apiCall };
};
Real-time Fleet Map
Interactive Map Component
- React + Leaflet
- Vue.js + Leaflet
// components/Map/FleetMap.jsx
import React, { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Polyline } from 'react-leaflet';
import L from 'leaflet';
import useRealtime from '../../hooks/useRealtime';
import useBookovia from '../../hooks/useBookovia';
// Custom vehicle icon
const vehicleIcon = new L.Icon({
iconUrl: '/icons/vehicle-marker.png',
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -16]
});
const FleetMap = () => {
const { apiCall } = useBookovia();
const { data: realtimeData } = useRealtime(['locations', 'trip_updates']);
const [vehicles, setVehicles] = useState([]);
const [routes, setRoutes] = useState({});
const [selectedVehicle, setSelectedVehicle] = useState(null);
// Fetch initial fleet data
useEffect(() => {
const fetchFleetData = async () => {
try {
const fleetData = await apiCall('fleet.getVehicles', {
include_location: true,
status: 'active'
});
setVehicles(fleetData.vehicles);
} catch (error) {
console.error('Failed to fetch fleet data:', error);
}
};
fetchFleetData();
// Refresh every 30 seconds
const interval = setInterval(fetchFleetData, 30000);
return () => clearInterval(interval);
}, [apiCall]);
// Update vehicles with real-time location data
useEffect(() => {
if (realtimeData.location_update) {
const locationUpdate = realtimeData.location_update;
setVehicles(prevVehicles =>
prevVehicles.map(vehicle =>
vehicle.id === locationUpdate.vehicle_id
? {
...vehicle,
last_location: locationUpdate.location,
last_update: locationUpdate.timestamp
}
: vehicle
)
);
}
}, [realtimeData.location_update]);
// Fetch route for selected vehicle
const fetchVehicleRoute = async (vehicleId, tripId) => {
if (routes[tripId]) return; // Already loaded
try {
const route = await apiCall('trips.getRoute', { trip_id: tripId });
setRoutes(prevRoutes => ({
...prevRoutes,
[tripId]: route.coordinates
}));
} catch (error) {
console.error('Failed to fetch route:', error);
}
};
const handleVehicleClick = (vehicle) => {
setSelectedVehicle(vehicle);
if (vehicle.current_trip_id) {
fetchVehicleRoute(vehicle.id, vehicle.current_trip_id);
}
};
return (
<div className="relative h-full w-full">
<MapContainer
center={[40.7128, -74.0060]}
zoom={12}
className="h-full w-full"
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{/* Vehicle markers */}
{vehicles.map(vehicle => (
<Marker
key={vehicle.id}
position={[
vehicle.last_location?.latitude || 0,
vehicle.last_location?.longitude || 0
]}
icon={vehicleIcon}
eventHandlers={{
click: () => handleVehicleClick(vehicle)
}}
>
<Popup>
<VehiclePopup vehicle={vehicle} />
</Popup>
</Marker>
))}
{/* Route polylines */}
{Object.entries(routes).map(([tripId, coordinates]) => (
<Polyline
key={tripId}
positions={coordinates}
color="blue"
weight={3}
opacity={0.7}
/>
))}
</MapContainer>
{/* Map controls */}
<MapControls
vehicles={vehicles}
selectedVehicle={selectedVehicle}
onVehicleSelect={setSelectedVehicle}
/>
</div>
);
};
const VehiclePopup = ({ vehicle }) => (
<div className="p-2 min-w-[200px]">
<h3 className="font-semibold text-lg">{vehicle.name}</h3>
<div className="space-y-1 text-sm">
<p><strong>Status:</strong> {vehicle.status}</p>
<p><strong>Driver:</strong> {vehicle.driver_name || 'Unassigned'}</p>
<p><strong>Speed:</strong> {vehicle.last_location?.speed || 0} km/h</p>
<p><strong>Last Update:</strong> {new Date(vehicle.last_update).toLocaleTimeString()}</p>
{vehicle.current_trip_id && (
<p><strong>Trip ID:</strong> {vehicle.current_trip_id}</p>
)}
</div>
</div>
);
const MapControls = ({ vehicles, selectedVehicle, onVehicleSelect }) => {
return (
<div className="absolute top-4 left-4 z-1000 bg-white rounded-lg shadow-lg p-4 max-w-sm">
<h3 className="font-semibold mb-2">Fleet Overview</h3>
<div className="space-y-2 max-h-64 overflow-y-auto">
{vehicles.map(vehicle => (
<div
key={vehicle.id}
className={`p-2 rounded cursor-pointer ${
selectedVehicle?.id === vehicle.id
? 'bg-blue-100 border border-blue-300'
: 'bg-gray-50 hover:bg-gray-100'
}`}
onClick={() => onVehicleSelect(vehicle)}
>
<div className="flex justify-between items-center">
<span className="font-medium">{vehicle.name}</span>
<span className={`px-2 py-1 rounded text-xs ${
vehicle.status === 'active' ? 'bg-green-200 text-green-800' :
vehicle.status === 'idle' ? 'bg-yellow-200 text-yellow-800' :
'bg-gray-200 text-gray-800'
}`}>
{vehicle.status}
</span>
</div>
<div className="text-sm text-gray-600">
{vehicle.driver_name || 'No driver assigned'}
</div>
</div>
))}
</div>
</div>
);
};
export default FleetMap;
<!-- components/Map/FleetMap.vue -->
<template>
<div class="relative h-full w-full">
<l-map
ref="map"
:zoom="zoom"
:center="center"
class="h-full w-full"
>
<l-tile-layer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
<!-- Vehicle markers -->
<l-marker
v-for="vehicle in vehicles"
:key="vehicle.id"
:lat-lng="[
vehicle.last_location?.latitude || 0,
vehicle.last_location?.longitude || 0
]"
:icon="vehicleIcon"
@click="handleVehicleClick(vehicle)"
>
<l-popup>
<VehiclePopup :vehicle="vehicle" />
</l-popup>
</l-marker>
<!-- Route polylines -->
<l-polyline
v-for="(coordinates, tripId) in routes"
:key="tripId"
:lat-lngs="coordinates"
color="blue"
:weight="3"
:opacity="0.7"
/>
</l-map>
<!-- Map controls -->
<MapControls
:vehicles="vehicles"
:selectedVehicle="selectedVehicle"
@vehicle-select="handleVehicleSelect"
/>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue';
import { LMap, LTileLayer, LMarker, LPopup, LPolyline } from '@vue-leaflet/vue-leaflet';
import L from 'leaflet';
import { useBookovia } from '../../composables/useBookovia';
import { useRealtime } from '../../composables/useRealtime';
import VehiclePopup from './VehiclePopup.vue';
import MapControls from './MapControls.vue';
const { apiCall } = useBookovia();
const { data: realtimeData } = useRealtime(['locations', 'trip_updates']);
const vehicles = ref([]);
const routes = ref({});
const selectedVehicle = ref(null);
const zoom = ref(12);
const center = ref([40.7128, -74.0060]);
// Custom vehicle icon
const vehicleIcon = new L.Icon({
iconUrl: '/icons/vehicle-marker.png',
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -16]
});
// Fetch initial fleet data
const fetchFleetData = async () => {
try {
const fleetData = await apiCall('fleet.getVehicles', {
include_location: true,
status: 'active'
});
vehicles.value = fleetData.vehicles;
} catch (error) {
console.error('Failed to fetch fleet data:', error);
}
};
// Fetch route for vehicle
const fetchVehicleRoute = async (vehicleId, tripId) => {
if (routes.value[tripId]) return;
try {
const route = await apiCall('trips.getRoute', { trip_id: tripId });
routes.value = {
...routes.value,
[tripId]: route.coordinates
};
} catch (error) {
console.error('Failed to fetch route:', error);
}
};
const handleVehicleClick = (vehicle) => {
selectedVehicle.value = vehicle;
if (vehicle.current_trip_id) {
fetchVehicleRoute(vehicle.id, vehicle.current_trip_id);
}
};
const handleVehicleSelect = (vehicle) => {
selectedVehicle.value = vehicle;
};
// Update vehicles with real-time data
watch(
() => realtimeData.value.location_update,
(locationUpdate) => {
if (locationUpdate) {
vehicles.value = vehicles.value.map(vehicle =>
vehicle.id === locationUpdate.vehicle_id
? {
...vehicle,
last_location: locationUpdate.location,
last_update: locationUpdate.timestamp
}
: vehicle
);
}
}
);
onMounted(() => {
fetchFleetData();
// Refresh every 30 seconds
setInterval(fetchFleetData, 30000);
});
</script>
Dashboard Overview
KPI Metrics Component
- React
- Vue.js
// components/Dashboard/Overview.jsx
import React, { useState, useEffect } from 'react';
import {
TruckIcon,
UserGroupIcon,
ExclamationTriangleIcon,
ChartBarIcon
} from '@heroicons/react/24/outline';
import useBookovia from '../../hooks/useBookovia';
import useRealtime from '../../hooks/useRealtime';
const DashboardOverview = () => {
const { apiCall } = useBookovia();
const { data: realtimeData } = useRealtime(['safety_events', 'trip_updates']);
const [metrics, setMetrics] = useState({
totalVehicles: 0,
activeTrips: 0,
totalDrivers: 0,
safetyScore: 0,
todayDistance: 0,
activeSafetyAlerts: 0
});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchDashboardData = async () => {
try {
setLoading(true);
// Fetch multiple endpoints in parallel
const [fleetOverview, safetyAnalytics, tripMetrics] = await Promise.all([
apiCall('fleet.getOverview'),
apiCall('analytics.getSafetyOverview', { period: 'today' }),
apiCall('analytics.getTripMetrics', { period: 'today' })
]);
setMetrics({
totalVehicles: fleetOverview.total_vehicles,
activeTrips: fleetOverview.active_trips,
totalDrivers: fleetOverview.total_drivers,
safetyScore: safetyAnalytics.average_score,
todayDistance: tripMetrics.total_distance,
activeSafetyAlerts: safetyAnalytics.active_alerts
});
} catch (error) {
console.error('Failed to fetch dashboard data:', error);
} finally {
setLoading(false);
}
};
fetchDashboardData();
// Refresh every minute
const interval = setInterval(fetchDashboardData, 60000);
return () => clearInterval(interval);
}, [apiCall]);
// Update metrics with real-time data
useEffect(() => {
if (realtimeData.safety_event) {
setMetrics(prev => ({
...prev,
activeSafetyAlerts: prev.activeSafetyAlerts + 1
}));
}
}, [realtimeData.safety_event]);
const metricCards = [
{
title: 'Total Vehicles',
value: metrics.totalVehicles,
icon: TruckIcon,
color: 'blue',
change: '+2 this week'
},
{
title: 'Active Trips',
value: metrics.activeTrips,
icon: ChartBarIcon,
color: 'green',
change: `${metrics.activeTrips} in progress`
},
{
title: 'Safety Score',
value: `${Math.round(metrics.safetyScore)}/100`,
icon: ExclamationTriangleIcon,
color: metrics.safetyScore > 80 ? 'green' : metrics.safetyScore > 60 ? 'yellow' : 'red',
change: metrics.safetyScore > 80 ? 'Excellent' : 'Needs attention'
},
{
title: 'Total Drivers',
value: metrics.totalDrivers,
icon: UserGroupIcon,
color: 'purple',
change: `${Math.round(metrics.totalDrivers * 0.8)} active today`
}
];
if (loading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[...Array(4)].map((_, i) => (
<div key={i} className="bg-white rounded-lg shadow p-6 animate-pulse">
<div className="h-4 bg-gray-200 rounded mb-4"></div>
<div className="h-8 bg-gray-200 rounded mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
))}
</div>
);
}
return (
<div className="space-y-6">
{/* KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{metricCards.map((metric, index) => (
<MetricCard key={index} {...metric} />
))}
</div>
{/* Today's Summary */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Today's Summary</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">
{Math.round(metrics.todayDistance)} km
</div>
<div className="text-gray-500">Total Distance</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-green-600">
{Math.round(metrics.todayDistance / (metrics.activeTrips || 1))} km
</div>
<div className="text-gray-500">Avg Trip Distance</div>
</div>
<div className="text-center">
<div className={`text-2xl font-bold ${
metrics.activeSafetyAlerts === 0 ? 'text-green-600' : 'text-red-600'
}`}>
{metrics.activeSafetyAlerts}
</div>
<div className="text-gray-500">Active Alerts</div>
</div>
</div>
</div>
</div>
);
};
const MetricCard = ({ title, value, icon: Icon, color, change }) => {
const colorClasses = {
blue: 'text-blue-600 bg-blue-100',
green: 'text-green-600 bg-green-100',
yellow: 'text-yellow-600 bg-yellow-100',
red: 'text-red-600 bg-red-100',
purple: 'text-purple-600 bg-purple-100'
};
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-gray-500">{title}</p>
<p className="text-2xl font-semibold text-gray-900">{value}</p>
<p className="text-xs text-gray-400 mt-1">{change}</p>
</div>
<div className={`p-3 rounded-lg ${colorClasses[color]}`}>
<Icon className="w-6 h-6" />
</div>
</div>
</div>
);
};
export default DashboardOverview;
<!-- components/Dashboard/Overview.vue -->
<template>
<div class="space-y-6">
<!-- KPI Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<MetricCard
v-for="(metric, index) in metricCards"
:key="index"
v-bind="metric"
/>
</div>
<!-- Today's Summary -->
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-semibold mb-4">Today's Summary</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="text-center">
<div class="text-2xl font-bold text-blue-600">
{{ Math.round(metrics.todayDistance) }} km
</div>
<div class="text-gray-500">Total Distance</div>
</div>
<div class="text-center">
<div class="text-2xl font-bold text-green-600">
{{ Math.round(metrics.todayDistance / (metrics.activeTrips || 1)) }} km
</div>
<div class="text-gray-500">Avg Trip Distance</div>
</div>
<div class="text-center">
<div :class="`text-2xl font-bold ${
metrics.activeSafetyAlerts === 0 ? 'text-green-600' : 'text-red-600'
}`">
{{ metrics.activeSafetyAlerts }}
</div>
<div class="text-gray-500">Active Alerts</div>
</div>
</div>
</div>
<!-- Live Activity Feed -->
<LiveActivityFeed :realtime-data="realtimeData" />
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import {
TruckIcon,
UserGroupIcon,
ExclamationTriangleIcon,
ChartBarIcon
} from '@heroicons/vue/24/outline';
import { useBookovia } from '../../composables/useBookovia';
import { useRealtime } from '../../composables/useRealtime';
import MetricCard from './MetricCard.vue';
import LiveActivityFeed from './LiveActivityFeed.vue';
const { apiCall } = useBookovia();
const { data: realtimeData } = useRealtime(['safety_events', 'trip_updates']);
const metrics = ref({
totalVehicles: 0,
activeTrips: 0,
totalDrivers: 0,
safetyScore: 0,
todayDistance: 0,
activeSafetyAlerts: 0
});
const loading = ref(true);
const metricCards = computed(() => [
{
title: 'Total Vehicles',
value: metrics.value.totalVehicles,
icon: TruckIcon,
color: 'blue',
change: '+2 this week'
},
{
title: 'Active Trips',
value: metrics.value.activeTrips,
icon: ChartBarIcon,
color: 'green',
change: `${metrics.value.activeTrips} in progress`
},
{
title: 'Safety Score',
value: `${Math.round(metrics.value.safetyScore)}/100`,
icon: ExclamationTriangleIcon,
color: metrics.value.safetyScore > 80 ? 'green' :
metrics.value.safetyScore > 60 ? 'yellow' : 'red',
change: metrics.value.safetyScore > 80 ? 'Excellent' : 'Needs attention'
},
{
title: 'Total Drivers',
value: metrics.value.totalDrivers,
icon: UserGroupIcon,
color: 'purple',
change: `${Math.round(metrics.value.totalDrivers * 0.8)} active today`
}
]);
const fetchDashboardData = async () => {
try {
loading.value = true;
const [fleetOverview, safetyAnalytics, tripMetrics] = await Promise.all([
apiCall('fleet.getOverview'),
apiCall('analytics.getSafetyOverview', { period: 'today' }),
apiCall('analytics.getTripMetrics', { period: 'today' })
]);
metrics.value = {
totalVehicles: fleetOverview.total_vehicles,
activeTrips: fleetOverview.active_trips,
totalDrivers: fleetOverview.total_drivers,
safetyScore: safetyAnalytics.average_score,
todayDistance: tripMetrics.total_distance,
activeSafetyAlerts: safetyAnalytics.active_alerts
};
} catch (error) {
console.error('Failed to fetch dashboard data:', error);
} finally {
loading.value = false;
}
};
// Update metrics with real-time data
watch(
() => realtimeData.value.safety_event,
(safetyEvent) => {
if (safetyEvent) {
metrics.value.activeSafetyAlerts += 1;
}
}
);
onMounted(() => {
fetchDashboardData();
// Refresh every minute
setInterval(fetchDashboardData, 60000);
});
</script>
Safety Analytics Dashboard
Safety Metrics and Charts
- React + Recharts
- Vue.js + Chart.js
// components/Dashboard/SafetyAnalytics.jsx
import React, { useState, useEffect } from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
PieChart,
Pie,
Cell,
BarChart,
Bar,
ResponsiveContainer
} from 'recharts';
import useBookovia from '../../hooks/useBookovia';
import useRealtime from '../../hooks/useRealtime';
const SafetyAnalytics = () => {
const { apiCall } = useBookovia();
const { data: realtimeData } = useRealtime(['safety_events']);
const [safetyData, setSafetyData] = useState({
scoreHistory: [],
eventDistribution: [],
driverRankings: [],
recentEvents: []
});
const [timeRange, setTimeRange] = useState('7d');
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchSafetyData = async () => {
try {
setLoading(true);
const [scoreHistory, eventStats, driverRankings, recentEvents] = await Promise.all([
apiCall('analytics.getSafetyScoreHistory', {
period: timeRange,
granularity: 'daily'
}),
apiCall('analytics.getSafetyEventDistribution', {
period: timeRange
}),
apiCall('analytics.getDriverRankings', {
metric: 'safety_score',
period: timeRange,
limit: 10
}),
apiCall('analytics.getRecentSafetyEvents', {
limit: 20,
severity: ['medium', 'high', 'critical']
})
]);
setSafetyData({
scoreHistory: scoreHistory.data,
eventDistribution: eventStats.data,
driverRankings: driverRankings.data,
recentEvents: recentEvents.data
});
} catch (error) {
console.error('Failed to fetch safety data:', error);
} finally {
setLoading(false);
}
};
fetchSafetyData();
}, [apiCall, timeRange]);
// Add new safety events to recent events
useEffect(() => {
if (realtimeData.safety_event) {
setSafetyData(prev => ({
...prev,
recentEvents: [realtimeData.safety_event, ...prev.recentEvents.slice(0, 19)]
}));
}
}, [realtimeData.safety_event]);
const COLORS = ['#10B981', '#F59E0B', '#EF4444', '#8B5CF6'];
if (loading) {
return <div className="animate-pulse">Loading safety analytics...</div>;
}
return (
<div className="space-y-6">
{/* Time Range Selector */}
<div className="flex justify-between items-center">
<h2 className="text-2xl font-bold">Safety Analytics</h2>
<div className="flex space-x-2">
{['1d', '7d', '30d', '90d'].map(range => (
<button
key={range}
onClick={() => setTimeRange(range)}
className={`px-4 py-2 rounded ${
timeRange === range
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`}
>
{range.replace('d', ' days')}
</button>
))}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Safety Score Trend */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Safety Score Trend</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={safetyData.scoreHistory}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="date"
tickFormatter={(value) => new Date(value).toLocaleDateString()}
/>
<YAxis domain={[0, 100]} />
<Tooltip
labelFormatter={(value) => new Date(value).toLocaleDateString()}
formatter={(value) => [`${value}`, 'Safety Score']}
/>
<Legend />
<Line
type="monotone"
dataKey="average_score"
stroke="#10B981"
strokeWidth={2}
name="Fleet Average"
/>
<Line
type="monotone"
dataKey="top_10_percent"
stroke="#3B82F6"
strokeWidth={2}
strokeDasharray="5 5"
name="Top 10%"
/>
</LineChart>
</ResponsiveContainer>
</div>
{/* Event Distribution */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Safety Event Distribution</h3>
<ResponsiveContainer width="100%" height={300}>
<PieChart>
<Pie
data={safetyData.eventDistribution}
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
outerRadius={80}
fill="#8884d8"
dataKey="count"
>
{safetyData.eventDistribution.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Driver Rankings */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Top Drivers by Safety Score</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={safetyData.driverRankings} layout="horizontal">
<CartesianGrid strokeDasharray="3 3" />
<XAxis type="number" domain={[0, 100]} />
<YAxis type="category" dataKey="driver_name" width={100} />
<Tooltip
formatter={(value) => [`${value}`, 'Safety Score']}
/>
<Bar dataKey="safety_score" fill="#10B981" />
</BarChart>
</ResponsiveContainer>
</div>
{/* Recent Safety Events */}
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-lg font-semibold mb-4">Recent Safety Events</h3>
<div className="space-y-3 max-h-72 overflow-y-auto">
{safetyData.recentEvents.map((event, index) => (
<SafetyEventItem key={index} event={event} />
))}
</div>
</div>
</div>
</div>
);
};
const SafetyEventItem = ({ event }) => {
const severityColors = {
low: 'bg-green-100 text-green-800',
medium: 'bg-yellow-100 text-yellow-800',
high: 'bg-orange-100 text-orange-800',
critical: 'bg-red-100 text-red-800'
};
const eventIcons = {
harsh_braking: '🛑',
harsh_acceleration: '🚀',
harsh_cornering: '↪️',
speeding: '💨',
phone_usage: '📱'
};
return (
<div className="flex items-center justify-between p-3 bg-gray-50 rounded">
<div className="flex items-center space-x-3">
<span className="text-xl">{eventIcons[event.type] || '⚠️'}</span>
<div>
<div className="font-medium">{event.driver_name}</div>
<div className="text-sm text-gray-500">
{event.type.replace('_', ' ')} • {new Date(event.timestamp).toLocaleTimeString()}
</div>
</div>
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${severityColors[event.severity]}`}>
{event.severity}
</span>
</div>
);
};
export default SafetyAnalytics;
<!-- components/Dashboard/SafetyAnalytics.vue -->
<template>
<div class="space-y-6">
<!-- Time Range Selector -->
<div class="flex justify-between items-center">
<h2 class="text-2xl font-bold">Safety Analytics</h2>
<div class="flex space-x-2">
<button
v-for="range in timeRanges"
:key="range.value"
@click="timeRange = range.value"
:class="`px-4 py-2 rounded ${
timeRange === range.value
? 'bg-blue-600 text-white'
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
}`"
>
{{ range.label }}
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Safety Score Trend -->
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-lg font-semibold mb-4">Safety Score Trend</h3>
<Line :data="scoreChartData" :options="scoreChartOptions" />
</div>
<!-- Event Distribution -->
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-lg font-semibold mb-4">Safety Event Distribution</h3>
<Doughnut :data="eventChartData" :options="eventChartOptions" />
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Driver Rankings -->
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-lg font-semibold mb-4">Top Drivers by Safety Score</h3>
<Bar :data="driverChartData" :options="driverChartOptions" />
</div>
<!-- Recent Safety Events -->
<div class="bg-white p-6 rounded-lg shadow">
<h3 class="text-lg font-semibold mb-4">Recent Safety Events</h3>
<div class="space-y-3 max-h-72 overflow-y-auto">
<SafetyEventItem
v-for="(event, index) in safetyData.recentEvents"
:key="index"
:event="event"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { Line, Doughnut, Bar } from 'vue-chartjs';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
ArcElement,
BarElement
} from 'chart.js';
import { useBookovia } from '../../composables/useBookovia';
import { useRealtime } from '../../composables/useRealtime';
import SafetyEventItem from './SafetyEventItem.vue';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
ArcElement,
BarElement
);
const { apiCall } = useBookovia();
const { data: realtimeData } = useRealtime(['safety_events']);
const timeRange = ref('7d');
const loading = ref(true);
const timeRanges = [
{ value: '1d', label: '1 Day' },
{ value: '7d', label: '7 Days' },
{ value: '30d', label: '30 Days' },
{ value: '90d', label: '90 Days' }
];
const safetyData = ref({
scoreHistory: [],
eventDistribution: [],
driverRankings: [],
recentEvents: []
});
const scoreChartData = computed(() => ({
labels: safetyData.value.scoreHistory.map(item =>
new Date(item.date).toLocaleDateString()
),
datasets: [
{
label: 'Fleet Average',
data: safetyData.value.scoreHistory.map(item => item.average_score),
borderColor: '#10B981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
tension: 0.4
},
{
label: 'Top 10%',
data: safetyData.value.scoreHistory.map(item => item.top_10_percent),
borderColor: '#3B82F6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
borderDash: [5, 5],
tension: 0.4
}
]
}));
const eventChartData = computed(() => ({
labels: safetyData.value.eventDistribution.map(item => item.name),
datasets: [{
data: safetyData.value.eventDistribution.map(item => item.count),
backgroundColor: ['#10B981', '#F59E0B', '#EF4444', '#8B5CF6']
}]
}));
const driverChartData = computed(() => ({
labels: safetyData.value.driverRankings.map(item => item.driver_name),
datasets: [{
label: 'Safety Score',
data: safetyData.value.driverRankings.map(item => item.safety_score),
backgroundColor: '#10B981'
}]
}));
const scoreChartOptions = {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
min: 0,
max: 100
}
}
};
const eventChartOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom'
}
}
};
const driverChartOptions = {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
scales: {
x: {
min: 0,
max: 100
}
}
};
const fetchSafetyData = async () => {
try {
loading.value = true;
const [scoreHistory, eventStats, driverRankings, recentEvents] = await Promise.all([
apiCall('analytics.getSafetyScoreHistory', {
period: timeRange.value,
granularity: 'daily'
}),
apiCall('analytics.getSafetyEventDistribution', {
period: timeRange.value
}),
apiCall('analytics.getDriverRankings', {
metric: 'safety_score',
period: timeRange.value,
limit: 10
}),
apiCall('analytics.getRecentSafetyEvents', {
limit: 20,
severity: ['medium', 'high', 'critical']
})
]);
safetyData.value = {
scoreHistory: scoreHistory.data,
eventDistribution: eventStats.data,
driverRankings: driverRankings.data,
recentEvents: recentEvents.data
};
} catch (error) {
console.error('Failed to fetch safety data:', error);
} finally {
loading.value = false;
}
};
// Add new safety events to recent events
watch(
() => realtimeData.value.safety_event,
(safetyEvent) => {
if (safetyEvent) {
safetyData.value.recentEvents = [
safetyEvent,
...safetyData.value.recentEvents.slice(0, 19)
];
}
}
);
watch(timeRange, () => {
fetchSafetyData();
});
onMounted(() => {
fetchSafetyData();
});
</script>
Real-time Notifications
Alert System
- React
// components/Notifications/AlertSystem.jsx
import React, { useState, useEffect } from 'react';
import {
XMarkIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
CheckCircleIcon
} from '@heroicons/react/24/outline';
import useRealtime from '../../hooks/useRealtime';
const AlertSystem = () => {
const { data: realtimeData } = useRealtime(['safety_events', 'emergency_alerts', 'system_notifications']);
const [alerts, setAlerts] = useState([]);
const [filter, setFilter] = useState('all'); // all, critical, high, medium
useEffect(() => {
// Process new safety events
if (realtimeData.safety_event) {
const alert = {
id: `safety_${Date.now()}`,
type: 'safety',
severity: realtimeData.safety_event.severity,
title: `${realtimeData.safety_event.type.replace('_', ' ')} detected`,
message: `Driver: ${realtimeData.safety_event.driver_name} - Vehicle: ${realtimeData.safety_event.vehicle_id}`,
timestamp: new Date(),
data: realtimeData.safety_event,
actions: [
{ label: 'View Details', action: 'view_details' },
{ label: 'Contact Driver', action: 'contact_driver' }
]
};
addAlert(alert);
}
// Process emergency alerts
if (realtimeData.emergency_alert) {
const alert = {
id: `emergency_${Date.now()}`,
type: 'emergency',
severity: 'critical',
title: 'Emergency Alert',
message: realtimeData.emergency_alert.message,
timestamp: new Date(),
data: realtimeData.emergency_alert,
actions: [
{ label: 'Dispatch Emergency Services', action: 'dispatch_emergency' },
{ label: 'Contact Driver', action: 'contact_driver' },
{ label: 'View Location', action: 'view_location' }
]
};
addAlert(alert);
// Play alert sound for critical alerts
playAlertSound('critical');
}
// Process system notifications
if (realtimeData.system_notification) {
const alert = {
id: `system_${Date.now()}`,
type: 'system',
severity: 'info',
title: realtimeData.system_notification.title,
message: realtimeData.system_notification.message,
timestamp: new Date(),
data: realtimeData.system_notification
};
addAlert(alert);
}
}, [realtimeData]);
const addAlert = (alert) => {
setAlerts(prev => [alert, ...prev.slice(0, 49)]); // Keep last 50 alerts
// Auto-dismiss info alerts after 10 seconds
if (alert.severity === 'info') {
setTimeout(() => {
dismissAlert(alert.id);
}, 10000);
}
};
const dismissAlert = (alertId) => {
setAlerts(prev => prev.filter(alert => alert.id !== alertId));
};
const handleAlertAction = async (alert, actionType) => {
switch (actionType) {
case 'view_details':
// Navigate to detailed view
window.open(`/safety-events/${alert.data.id}`, '_blank');
break;
case 'contact_driver':
// Initiate driver contact
if (alert.data.driver_phone) {
window.open(`tel:${alert.data.driver_phone}`);
}
break;
case 'dispatch_emergency':
// Trigger emergency dispatch
await dispatchEmergencyServices(alert.data);
break;
case 'view_location':
// Show location on map
showLocationOnMap(alert.data.location);
break;
}
// Mark alert as acknowledged
setAlerts(prev =>
prev.map(a =>
a.id === alert.id
? { ...a, acknowledged: true }
: a
)
);
};
const playAlertSound = (severity) => {
if ('Notification' in window) {
new Notification(`${severity.toUpperCase()} Alert`, {
body: 'Check the dashboard for details',
icon: '/icons/alert.png'
});
}
// Play different sounds based on severity
const audioMap = {
critical: '/sounds/critical-alert.mp3',
high: '/sounds/high-alert.mp3',
medium: '/sounds/medium-alert.mp3'
};
if (audioMap[severity]) {
const audio = new Audio(audioMap[severity]);
audio.play().catch(e => console.log('Could not play alert sound:', e));
}
};
const filteredAlerts = alerts.filter(alert => {
if (filter === 'all') return true;
return alert.severity === filter;
});
const alertCounts = {
all: alerts.length,
critical: alerts.filter(a => a.severity === 'critical').length,
high: alerts.filter(a => a.severity === 'high').length,
medium: alerts.filter(a => a.severity === 'medium').length
};
return (
<div className="bg-white rounded-lg shadow">
{/* Header */}
<div className="p-4 border-b">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold">Live Alerts</h3>
<div className="flex space-x-2">
{Object.entries(alertCounts).map(([severity, count]) => (
<button
key={severity}
onClick={() => setFilter(severity)}
className={`px-3 py-1 rounded text-sm ${
filter === severity
? getSeverityButtonClass(severity, true)
: getSeverityButtonClass(severity, false)
}`}
>
{severity} ({count})
</button>
))}
</div>
</div>
</div>
{/* Alerts List */}
<div className="max-h-96 overflow-y-auto">
{filteredAlerts.length === 0 ? (
<div className="p-8 text-center text-gray-500">
<InformationCircleIcon className="w-12 h-12 mx-auto mb-2 text-gray-400" />
<p>No alerts to display</p>
</div>
) : (
<div className="divide-y">
{filteredAlerts.map(alert => (
<AlertItem
key={alert.id}
alert={alert}
onDismiss={dismissAlert}
onAction={handleAlertAction}
/>
))}
</div>
)}
</div>
</div>
);
};
const AlertItem = ({ alert, onDismiss, onAction }) => {
const getSeverityIcon = (severity) => {
switch (severity) {
case 'critical':
return <ExclamationTriangleIcon className="w-5 h-5 text-red-500" />;
case 'high':
return <ExclamationTriangleIcon className="w-5 h-5 text-orange-500" />;
case 'medium':
return <ExclamationTriangleIcon className="w-5 h-5 text-yellow-500" />;
default:
return <InformationCircleIcon className="w-5 h-5 text-blue-500" />;
}
};
const getSeverityBg = (severity) => {
switch (severity) {
case 'critical':
return 'bg-red-50 border-l-red-500';
case 'high':
return 'bg-orange-50 border-l-orange-500';
case 'medium':
return 'bg-yellow-50 border-l-yellow-500';
default:
return 'bg-blue-50 border-l-blue-500';
}
};
return (
<div className={`p-4 border-l-4 ${getSeverityBg(alert.severity)} ${
alert.acknowledged ? 'opacity-60' : ''
}`}>
<div className="flex justify-between items-start">
<div className="flex items-start space-x-3 flex-1">
{getSeverityIcon(alert.severity)}
<div className="flex-1">
<div className="flex justify-between items-center mb-1">
<h4 className="font-medium text-gray-900">{alert.title}</h4>
<span className="text-xs text-gray-500">
{alert.timestamp.toLocaleTimeString()}
</span>
</div>
<p className="text-sm text-gray-600 mb-2">{alert.message}</p>
{/* Action buttons */}
{alert.actions && (
<div className="flex flex-wrap gap-2">
{alert.actions.map((action, index) => (
<button
key={index}
onClick={() => onAction(alert, action.action)}
className="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded"
>
{action.label}
</button>
))}
</div>
)}
</div>
</div>
{/* Dismiss button */}
<button
onClick={() => onDismiss(alert.id)}
className="ml-3 text-gray-400 hover:text-gray-600"
>
<XMarkIcon className="w-4 h-4" />
</button>
</div>
</div>
);
};
const getSeverityButtonClass = (severity, active) => {
const baseClass = active ? 'text-white' : 'text-gray-600 bg-gray-100 hover:bg-gray-200';
if (!active) return baseClass;
switch (severity) {
case 'all':
return 'bg-gray-600 text-white';
case 'critical':
return 'bg-red-600 text-white';
case 'high':
return 'bg-orange-600 text-white';
case 'medium':
return 'bg-yellow-600 text-white';
default:
return 'bg-blue-600 text-white';
}
};
const dispatchEmergencyServices = async (alertData) => {
// Implementation would integrate with emergency services API
console.log('Dispatching emergency services for:', alertData);
};
const showLocationOnMap = (location) => {
// Implementation would focus map on location
console.log('Showing location on map:', location);
};
export default AlertSystem;
Reports and Analytics
Custom Report Builder
- React
// components/Reports/ReportBuilder.jsx
import React, { useState, useEffect } from 'react';
import {
CalendarIcon,
DocumentArrowDownIcon,
ChartBarIcon,
Cog6ToothIcon
} from '@heroicons/react/24/outline';
import useBookovia from '../../hooks/useBookovia';
const ReportBuilder = () => {
const { apiCall } = useBookovia();
const [reportConfig, setReportConfig] = useState({
type: 'safety_summary',
dateRange: {
start: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
end: new Date().toISOString().split('T')[0]
},
filters: {
vehicles: [],
drivers: [],
regions: []
},
metrics: ['safety_score', 'trip_count', 'distance', 'fuel_consumption'],
format: 'pdf',
groupBy: 'driver',
includeCharts: true
});
const [availableOptions, setAvailableOptions] = useState({
vehicles: [],
drivers: [],
regions: []
});
const [generating, setGenerating] = useState(false);
const [reportPreview, setReportPreview] = useState(null);
const reportTypes = [
{ value: 'safety_summary', label: 'Safety Summary Report' },
{ value: 'trip_analysis', label: 'Trip Analysis Report' },
{ value: 'driver_performance', label: 'Driver Performance Report' },
{ value: 'fleet_utilization', label: 'Fleet Utilization Report' },
{ value: 'fuel_efficiency', label: 'Fuel Efficiency Report' },
{ value: 'compliance', label: 'Compliance Report' }
];
const metrics = [
{ value: 'safety_score', label: 'Safety Score' },
{ value: 'trip_count', label: 'Trip Count' },
{ value: 'distance', label: 'Total Distance' },
{ value: 'duration', label: 'Total Duration' },
{ value: 'fuel_consumption', label: 'Fuel Consumption' },
{ value: 'harsh_events', label: 'Harsh Events' },
{ value: 'idle_time', label: 'Idle Time' },
{ value: 'speed_violations', label: 'Speed Violations' }
];
useEffect(() => {
const fetchOptions = async () => {
try {
const [vehicles, drivers, regions] = await Promise.all([
apiCall('fleet.getVehicles'),
apiCall('fleet.getDrivers'),
apiCall('fleet.getRegions')
]);
setAvailableOptions({
vehicles: vehicles.data,
drivers: drivers.data,
regions: regions.data
});
} catch (error) {
console.error('Failed to fetch report options:', error);
}
};
fetchOptions();
}, [apiCall]);
const handleConfigChange = (field, value) => {
setReportConfig(prev => ({
...prev,
[field]: value
}));
};
const handleFilterChange = (filterType, values) => {
setReportConfig(prev => ({
...prev,
filters: {
...prev.filters,
[filterType]: values
}
}));
};
const generateReport = async () => {
setGenerating(true);
try {
const response = await apiCall('reports.generate', {
...reportConfig,
async: true // Generate asynchronously for large reports
});
if (response.async) {
// Poll for completion
const reportId = response.report_id;
let completed = false;
while (!completed) {
await new Promise(resolve => setTimeout(resolve, 2000));
const status = await apiCall('reports.getStatus', { report_id: reportId });
if (status.status === 'completed') {
completed = true;
if (reportConfig.format === 'pdf') {
// Download PDF
const blob = await apiCall('reports.download', {
report_id: reportId,
format: 'pdf'
});
downloadFile(blob, `${reportConfig.type}_${Date.now()}.pdf`);
} else {
// Show preview for other formats
const data = await apiCall('reports.getData', { report_id: reportId });
setReportPreview(data);
}
} else if (status.status === 'failed') {
throw new Error(status.error || 'Report generation failed');
}
}
} else {
// Immediate response for small reports
setReportPreview(response.data);
}
} catch (error) {
console.error('Report generation failed:', error);
alert('Failed to generate report: ' + error.message);
} finally {
setGenerating(false);
}
};
const downloadFile = (blob, filename) => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
};
const scheduleReport = async () => {
try {
await apiCall('reports.schedule', {
...reportConfig,
schedule: {
frequency: 'weekly',
day_of_week: 1, // Monday
time: '09:00',
recipients: ['fleet@company.com']
}
});
alert('Report scheduled successfully!');
} catch (error) {
console.error('Failed to schedule report:', error);
}
};
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold mb-6">Custom Report Builder</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Report Configuration */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Report Configuration</h3>
{/* Report Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Report Type
</label>
<select
value={reportConfig.type}
onChange={(e) => handleConfigChange('type', e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md"
>
{reportTypes.map(type => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
{/* Date Range */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Start Date
</label>
<input
type="date"
value={reportConfig.dateRange.start}
onChange={(e) => handleConfigChange('dateRange', {
...reportConfig.dateRange,
start: e.target.value
})}
className="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
End Date
</label>
<input
type="date"
value={reportConfig.dateRange.end}
onChange={(e) => handleConfigChange('dateRange', {
...reportConfig.dateRange,
end: e.target.value
})}
className="w-full p-2 border border-gray-300 rounded-md"
/>
</div>
</div>
{/* Metrics Selection */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Metrics to Include
</label>
<div className="space-y-2 max-h-32 overflow-y-auto border border-gray-200 rounded p-2">
{metrics.map(metric => (
<label key={metric.value} className="flex items-center">
<input
type="checkbox"
checked={reportConfig.metrics.includes(metric.value)}
onChange={(e) => {
const newMetrics = e.target.checked
? [...reportConfig.metrics, metric.value]
: reportConfig.metrics.filter(m => m !== metric.value);
handleConfigChange('metrics', newMetrics);
}}
className="mr-2"
/>
{metric.label}
</label>
))}
</div>
</div>
{/* Format and Options */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Format
</label>
<select
value={reportConfig.format}
onChange={(e) => handleConfigChange('format', e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md"
>
<option value="pdf">PDF</option>
<option value="excel">Excel</option>
<option value="csv">CSV</option>
<option value="json">JSON</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Group By
</label>
<select
value={reportConfig.groupBy}
onChange={(e) => handleConfigChange('groupBy', e.target.value)}
className="w-full p-2 border border-gray-300 rounded-md"
>
<option value="driver">Driver</option>
<option value="vehicle">Vehicle</option>
<option value="region">Region</option>
<option value="date">Date</option>
</select>
</div>
</div>
{/* Include Charts */}
<div>
<label className="flex items-center">
<input
type="checkbox"
checked={reportConfig.includeCharts}
onChange={(e) => handleConfigChange('includeCharts', e.target.checked)}
className="mr-2"
/>
Include Charts and Visualizations
</label>
</div>
</div>
{/* Filters */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Filters</h3>
{/* Vehicle Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Vehicles (leave empty for all)
</label>
<select
multiple
value={reportConfig.filters.vehicles}
onChange={(e) => handleFilterChange('vehicles',
Array.from(e.target.selectedOptions, option => option.value)
)}
className="w-full p-2 border border-gray-300 rounded-md h-24"
>
{availableOptions.vehicles.map(vehicle => (
<option key={vehicle.id} value={vehicle.id}>
{vehicle.name}
</option>
))}
</select>
</div>
{/* Driver Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Drivers (leave empty for all)
</label>
<select
multiple
value={reportConfig.filters.drivers}
onChange={(e) => handleFilterChange('drivers',
Array.from(e.target.selectedOptions, option => option.value)
)}
className="w-full p-2 border border-gray-300 rounded-md h-24"
>
{availableOptions.drivers.map(driver => (
<option key={driver.id} value={driver.id}>
{driver.name}
</option>
))}
</select>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="mt-6 flex flex-wrap gap-4">
<button
onClick={generateReport}
disabled={generating}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
<DocumentArrowDownIcon className="w-4 h-4 mr-2" />
{generating ? 'Generating...' : 'Generate Report'}
</button>
<button
onClick={scheduleReport}
className="flex items-center px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
>
<CalendarIcon className="w-4 h-4 mr-2" />
Schedule Report
</button>
<button
onClick={() => setReportPreview(null)}
className="flex items-center px-4 py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700"
>
<ChartBarIcon className="w-4 h-4 mr-2" />
Preview Data
</button>
</div>
</div>
{/* Report Preview */}
{reportPreview && (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Report Preview</h3>
<ReportPreview data={reportPreview} config={reportConfig} />
</div>
)}
</div>
);
};
const ReportPreview = ({ data, config }) => {
return (
<div className="space-y-4">
<div className="text-sm text-gray-600">
Report Type: {config.type} |
Date Range: {config.dateRange.start} to {config.dateRange.end} |
Records: {data.length}
</div>
<div className="overflow-x-auto">
<table className="min-w-full table-auto">
<thead>
<tr className="bg-gray-50">
{config.metrics.map(metric => (
<th key={metric} className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
{metric.replace('_', ' ')}
</th>
))}
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{data.slice(0, 10).map((row, index) => (
<tr key={index}>
{config.metrics.map(metric => (
<td key={metric} className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{row[metric] || '-'}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
{data.length > 10 && (
<div className="text-sm text-gray-500 text-center">
Showing first 10 of {data.length} records
</div>
)}
</div>
);
};
export default ReportBuilder;
Deployment and Performance
Production Optimization
- Next.js Deployment
- React Optimization
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
// Enable compression
compress: true,
// Optimize images
images: {
domains: ['api.bookovia.com'],
formats: ['image/avif', 'image/webp'],
},
// Environment variables
env: {
BOOKOVIA_API_URL: process.env.BOOKOVIA_API_URL,
BOOKOVIA_WS_URL: process.env.BOOKOVIA_WS_URL,
},
// Webpack optimizations
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// Analyze bundle in production
if (!dev && !isServer) {
config.plugins.push(
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 50
})
);
}
return config;
},
// Headers for security and caching
async headers() {
return [
{
source: '/api/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=300, stale-while-revalidate=600'
}
]
}
];
},
// Redirects
async redirects() {
return [
{
source: '/dashboard',
destination: '/dashboard/overview',
permanent: true
}
];
}
};
module.exports = nextConfig;
// lib/monitoring.js
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
// Send to your analytics service
if (process.env.NODE_ENV === 'production') {
fetch('/api/analytics/web-vitals', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(metric),
});
}
}
export function reportWebVitals(metric) {
console.log(metric);
sendToAnalytics(metric);
}
// Initialize performance monitoring
export function initializeMonitoring() {
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
}
// Performance optimization hooks
import { useMemo, useCallback, memo } from 'react';
// Memoized fleet data processing
export const useOptimizedFleetData = (vehicles, filters) => {
return useMemo(() => {
if (!vehicles) return [];
return vehicles
.filter(vehicle => {
if (filters.status && vehicle.status !== filters.status) return false;
if (filters.region && vehicle.region !== filters.region) return false;
return true;
})
.sort((a, b) => {
// Sort by last update time, most recent first
return new Date(b.last_update) - new Date(a.last_update);
});
}, [vehicles, filters]);
};
// Memoized chart data processing
export const useChartData = (rawData, chartType) => {
return useMemo(() => {
switch (chartType) {
case 'safety_trend':
return rawData.map(item => ({
date: new Date(item.date).toLocaleDateString(),
score: item.average_score,
target: 85 // Company target
}));
case 'event_distribution':
const eventCounts = rawData.reduce((acc, event) => {
acc[event.type] = (acc[event.type] || 0) + 1;
return acc;
}, {});
return Object.entries(eventCounts).map(([type, count]) => ({
name: type.replace('_', ' '),
value: count
}));
default:
return rawData;
}
}, [rawData, chartType]);
};
// Optimized map component
export const OptimizedFleetMap = memo(({ vehicles, selectedVehicle, onVehicleSelect }) => {
const handleVehicleClick = useCallback((vehicle) => {
onVehicleSelect(vehicle.id);
}, [onVehicleSelect]);
const visibleVehicles = useMemo(() => {
// Only render vehicles in viewport
return vehicles.filter(vehicle =>
vehicle.last_location &&
vehicle.last_location.latitude &&
vehicle.last_location.longitude
);
}, [vehicles]);
return (
<div className="h-full w-full">
{/* Map implementation with optimized rendering */}
</div>
);
});
// Virtual scrolling for large lists
import { FixedSizeList as List } from 'react-window';
export const VirtualizedVehicleList = ({ vehicles, onVehicleSelect }) => {
const Row = ({ index, style }) => (
<div style={style} className="p-2 border-b">
<VehicleListItem
vehicle={vehicles[index]}
onClick={() => onVehicleSelect(vehicles[index])}
/>
</div>
);
return (
<List
height={400}
itemCount={vehicles.length}
itemSize={80}
itemData={vehicles}
>
{Row}
</List>
);
};
Best Practices
Security and Performance
Security Best Practices
Security Best Practices
- API Key Management - Never expose API keys in frontend code
- HTTPS Only - Always use HTTPS for production deployments
- Input Validation - Validate all user inputs on both client and server
- Authentication - Implement proper user authentication and authorization
- CORS Configuration - Configure CORS policies appropriately
- Content Security Policy - Implement CSP headers to prevent XSS attacks
Performance Optimization
Performance Optimization
- Code Splitting - Split code by routes and components
- Lazy Loading - Load components and data on demand
- Caching Strategy - Cache API responses and static assets
- Image Optimization - Compress and resize images appropriately
- Bundle Analysis - Regularly analyze and optimize bundle size
- Memory Management - Clean up event listeners and subscriptions
Real-time Data Handling
Real-time Data Handling
- Connection Management - Handle WebSocket reconnections gracefully
- Data Throttling - Limit update frequency for performance
- Memory Cleanup - Remove old real-time data to prevent memory leaks
- Error Boundaries - Implement error boundaries for real-time components
- Offline Support - Handle network disconnections gracefully
- Data Validation - Validate incoming real-time data
Testing Strategy
Unit Testing
Unit Testing
- Test individual components and hooks
- Mock API calls and WebSocket connections
- Test data processing and transformation functions
- Verify error handling and edge cases
Integration Testing
Integration Testing
- Test component interactions and data flow
- Verify API integration and error handling
- Test real-time data updates and WebSocket behavior
- Validate user workflows and navigation
End-to-End Testing
End-to-End Testing
- Test complete user journeys
- Verify cross-browser compatibility
- Test responsive design on different screen sizes
- Validate performance under load
Next Steps
Mobile Integration
Learn how to build mobile apps that complement your web dashboard
Fleet Management
Advanced fleet management features and optimization strategies
Real-time Streaming
Deep dive into real-time data streaming and WebSocket integration
API Reference
Explore the complete Bookovia API documentation
Ready to build your fleet dashboard? Start with our quickstart guide to set up your development environment and make your first API call.