Auto refresh and static history json
This commit is contained in:
@@ -6,413 +6,387 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
var logsData = JSON.parse(telemetryEl.getAttribute('data-logs') || '[]');
|
var logsData = JSON.parse(telemetryEl.getAttribute('data-logs') || '[]');
|
||||||
var googleMapsApiKey = telemetryEl.getAttribute('data-gmaps-key') || '';
|
var googleMapsApiKey = telemetryEl.getAttribute('data-gmaps-key') || '';
|
||||||
|
|
||||||
var map;
|
// Arka planda sunucudan JSON rotasını çek
|
||||||
var activePolylines = [];
|
fetch('/mymach_fleet_intelligence/static/src/data/history.json?t=' + new Date().getTime())
|
||||||
var activeMarkers = [];
|
.then(function(res) {
|
||||||
|
if (!res.ok) throw new Error('History not found');
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(function(historyData) {
|
||||||
|
initDashboard(historyData);
|
||||||
|
})
|
||||||
|
.catch(function(e) {
|
||||||
|
// Dosya yoksa veya hata varsa boş rotayla başlat
|
||||||
|
initDashboard([]);
|
||||||
|
});
|
||||||
|
|
||||||
var isGoogleLoaded = false;
|
function initDashboard(serverHistory) {
|
||||||
if (googleMapsApiKey) {
|
var map;
|
||||||
if (typeof google !== 'undefined' && google.maps) {
|
var activePolylines = [];
|
||||||
isGoogleLoaded = true;
|
var activeMarkers = [];
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGoogleLoaded) {
|
var isGoogleLoaded = false;
|
||||||
map = new google.maps.Map(document.getElementById('map'), {
|
if (googleMapsApiKey) {
|
||||||
center: {lat: 41.0766, lng: 29.0124},
|
if (typeof google !== 'undefined' && google.maps) {
|
||||||
zoom: 12,
|
isGoogleLoaded = true;
|
||||||
mapTypeId: 'hybrid', // Google Uydu + Yol Haritası
|
}
|
||||||
zoomControl: true,
|
|
||||||
zoomControlOptions: { position: google.maps.ControlPosition.RIGHT_BOTTOM },
|
|
||||||
mapTypeControl: false
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
map = L.map('map', { zoomControl: false }).setView([41.0766, 29.0124], 12);
|
|
||||||
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
|
||||||
|
|
||||||
// Yüksek Detaylı Esri World Imagery Uydu Haritası Katmanı
|
|
||||||
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
|
||||||
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
|
|
||||||
maxZoom: 19
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
// Şehir ve Yol Etiketleri Katmanı (Uydu üzerine)
|
|
||||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png', {
|
|
||||||
subdomains: 'abcd',
|
|
||||||
maxZoom: 19
|
|
||||||
}).addTo(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mavi Başlangıç İğnesi (Google Maps Stilinde)
|
|
||||||
function createBlueStartIcon() {
|
|
||||||
return L.divIcon({
|
|
||||||
className: 'custom-blue-start-marker',
|
|
||||||
html: '<div style="position: relative;">' +
|
|
||||||
'<div style="background: #2563eb; border: 2px solid white; width: 26px; height: 26px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); box-shadow: 0 4px 14px rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center;">' +
|
|
||||||
'<div style="background: white; width: 9px; height: 9px; border-radius: 50%; transform: rotate(45deg);"></div>' +
|
|
||||||
'</div></div>',
|
|
||||||
iconSize: [26, 26],
|
|
||||||
iconAnchor: [13, 26]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kırmızı Bitiş Hedef İğnesi (Google Maps Stilinde)
|
|
||||||
function createRedEndIcon() {
|
|
||||||
return L.divIcon({
|
|
||||||
className: 'custom-red-end-marker',
|
|
||||||
html: '<div style="position: relative;">' +
|
|
||||||
'<div style="background: #ef4444; border: 2px solid white; width: 26px; height: 26px; border-radius: 50% 50% 50% 0; transform: rotate(-45deg); box-shadow: 0 4px 14px rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center;">' +
|
|
||||||
'<div style="background: white; width: 9px; height: 9px; border-radius: 50%; transform: rotate(45deg);"></div>' +
|
|
||||||
'</div></div>',
|
|
||||||
iconSize: [26, 26],
|
|
||||||
iconAnchor: [13, 26]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yol Üzeri Türkçe Çağrı Kutucuğu (Callout Badge)
|
|
||||||
function createCalloutIcon(text, bgHex) {
|
|
||||||
var bg = bgHex || '#2563eb';
|
|
||||||
return L.divIcon({
|
|
||||||
className: 'custom-callout-marker',
|
|
||||||
html: '<div style="background: ' + bg + '; color: white; padding: 6px 12px; border-radius: 16px; font-weight: 800; font-size: 11px; box-shadow: 0 4px 15px rgba(0,0,0,0.7); border: 2px solid white; font-family: sans-serif; white-space: nowrap;">' + text + '</div>',
|
|
||||||
iconSize: [130, 26],
|
|
||||||
iconAnchor: [65, 13]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var vehicleAnimationTimer = null;
|
|
||||||
|
|
||||||
// Canlı Hareket Eden Araç İkonu (SVG 🚘 + Anlık Hız Göstergeli)
|
|
||||||
function createLiveCarIcon(driverTag, speedText) {
|
|
||||||
return L.divIcon({
|
|
||||||
className: 'custom-live-car-marker',
|
|
||||||
html: '<div style="position: relative; text-align: center;">' +
|
|
||||||
'<div style="background: rgba(15, 23, 42, 0.9); color: #38bdf8; padding: 4px 9px; border-radius: 12px; font-size: 10px; font-weight: 800; border: 1.5px solid #38bdf8; box-shadow: 0 4px 14px rgba(0,0,0,0.8); white-space: nowrap; margin-bottom: 4px; display: inline-block;">' +
|
|
||||||
'🚘 ' + (driverTag || 'Sürücü') + ' • <span style="color: #10b981;">' + (speedText || '55 km/s') + '</span>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div style="width: 30px; height: 30px; background: #0284c7; border: 2.5px solid white; border-radius: 50%; box-shadow: 0 0 18px #0284c7; margin: 0 auto; display: flex; align-items: center; justify-content: center;">' +
|
|
||||||
'<span style="font-size: 15px;">🚗</span>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>',
|
|
||||||
iconSize: [140, 55],
|
|
||||||
iconAnchor: [70, 52]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function animateMovingVehicle(routePoints, vehicle) {
|
|
||||||
if (vehicleAnimationTimer) {
|
|
||||||
clearInterval(vehicleAnimationTimer);
|
|
||||||
vehicleAnimationTimer = null;
|
|
||||||
}
|
|
||||||
if (!routePoints || routePoints.length === 0) return;
|
|
||||||
|
|
||||||
// Canlı Takip Modu: Arabayı her zaman rotanın EN SON (en güncel) noktasına koy.
|
|
||||||
var lastPoint = routePoints[routePoints.length - 1];
|
|
||||||
var speedStr = vehicle.gps_speed ? vehicle.gps_speed + " km/s" : "0 km/s";
|
|
||||||
|
|
||||||
var staticCar = L.marker(lastPoint, {
|
|
||||||
icon: createLiveCarIcon(vehicle.driver_tag, speedStr)
|
|
||||||
}).addTo(map);
|
|
||||||
activeMarkers.push(staticCar);
|
|
||||||
|
|
||||||
// Haritayı doğrudan bu son noktaya odakla (Zoom)
|
|
||||||
map.setView(lastPoint, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectDriver(vehicle) {
|
|
||||||
if (vehicleAnimationTimer) {
|
|
||||||
clearInterval(vehicleAnimationTimer);
|
|
||||||
vehicleAnimationTimer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGoogleLoaded) {
|
if (isGoogleLoaded) {
|
||||||
activePolylines.forEach(function(p) { p.setMap(null); });
|
map = new google.maps.Map(document.getElementById('map'), {
|
||||||
activeMarkers.forEach(function(m) { m.setMap(null); });
|
center: {lat: 41.0766, lng: 29.0124},
|
||||||
|
zoom: 12,
|
||||||
|
mapTypeId: 'hybrid', // Google Uydu + Yol Haritası
|
||||||
|
zoomControl: true,
|
||||||
|
zoomControlOptions: { position: google.maps.ControlPosition.RIGHT_BOTTOM },
|
||||||
|
mapTypeControl: false
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
activePolylines.forEach(function(p) { map.removeLayer(p); });
|
map = L.map('map', { zoomControl: false }).setView([41.0766, 29.0124], 12);
|
||||||
activeMarkers.forEach(function(m) { map.removeLayer(m); });
|
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||||
|
|
||||||
|
// Yüksek Detaylı Esri World Imagery Uydu Haritası Katmanı
|
||||||
|
L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||||
|
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
|
||||||
|
maxZoom: 19
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Şehir ve Yol Etiketleri Katmanı (Uydu üzerine)
|
||||||
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png', {
|
||||||
|
subdomains: 'abcd',
|
||||||
|
maxZoom: 19
|
||||||
|
}).addTo(map);
|
||||||
}
|
}
|
||||||
activePolylines = [];
|
|
||||||
activeMarkers = [];
|
|
||||||
|
|
||||||
var greenPts = vehicle.green_route || [];
|
// Yol Üzeri Türkçe Çağrı Kutucuğu (Callout Badge)
|
||||||
var redPts = vehicle.red_route || [];
|
function createCalloutIcon(text, bgHex) {
|
||||||
var alt1Pts = vehicle.alt1 || [];
|
var bg = bgHex || '#2563eb';
|
||||||
var alt2Pts = vehicle.alt2 || [];
|
return L.divIcon({
|
||||||
|
className: 'custom-callout-marker',
|
||||||
if (isGoogleLoaded) {
|
html: '<div style="background: ' + bg + '; color: white; padding: 6px 12px; border-radius: 16px; font-weight: 800; font-size: 11px; box-shadow: 0 4px 15px rgba(0,0,0,0.7); border: 2px solid white; font-family: sans-serif; white-space: nowrap;">' + text + '</div>',
|
||||||
if (greenPts.length > 0) {
|
iconSize: [130, 26],
|
||||||
var polyGreen = new google.maps.Polyline({
|
iconAnchor: [65, 13]
|
||||||
path: greenPts.map(function(p) { return {lat: p[0], lng: p[1]}; }),
|
|
||||||
strokeColor: '#10b981',
|
|
||||||
strokeOpacity: 0.95,
|
|
||||||
strokeWeight: 7,
|
|
||||||
map: map
|
|
||||||
});
|
|
||||||
activePolylines.push(polyGreen);
|
|
||||||
}
|
|
||||||
if (redPts.length > 0) {
|
|
||||||
var polyRed = new google.maps.Polyline({
|
|
||||||
path: redPts.map(function(p) { return {lat: p[0], lng: p[1]}; }),
|
|
||||||
strokeColor: '#ef4444',
|
|
||||||
strokeOpacity: 0.95,
|
|
||||||
strokeWeight: 7,
|
|
||||||
map: map
|
|
||||||
});
|
|
||||||
activePolylines.push(polyRed);
|
|
||||||
}
|
|
||||||
var polyAlt1 = new google.maps.Polyline({
|
|
||||||
path: alt1Pts.map(function(p) { return {lat: p[0], lng: p[1]}; }),
|
|
||||||
strokeColor: '#64748b',
|
|
||||||
strokeOpacity: 0.55,
|
|
||||||
strokeWeight: 4,
|
|
||||||
map: map
|
|
||||||
});
|
});
|
||||||
var polyAlt2 = new google.maps.Polyline({
|
}
|
||||||
path: alt2Pts.map(function(p) { return {lat: p[0], lng: p[1]}; }),
|
|
||||||
strokeColor: '#64748b',
|
var vehicleAnimationTimer = null;
|
||||||
strokeOpacity: 0.55,
|
|
||||||
strokeWeight: 4,
|
// Canlı Hareket Eden Araç İkonu (SVG 🚘 + Anlık Hız Göstergeli)
|
||||||
map: map
|
function createLiveCarIcon(driverTag, speedText) {
|
||||||
|
return L.divIcon({
|
||||||
|
className: 'custom-live-car-marker',
|
||||||
|
html: '<div style="position: relative; text-align: center;">' +
|
||||||
|
'<div style="background: rgba(15, 23, 42, 0.9); color: #38bdf8; padding: 4px 9px; border-radius: 12px; font-size: 10px; font-weight: 800; border: 1.5px solid #38bdf8; box-shadow: 0 4px 14px rgba(0,0,0,0.8); white-space: nowrap; margin-bottom: 4px; display: inline-block;">' +
|
||||||
|
'🚘 ' + (driverTag || 'Sürücü') + ' • <span style="color: #10b981;">' + (speedText || '55 km/s') + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="width: 30px; height: 30px; background: #0284c7; border: 2.5px solid white; border-radius: 50%; box-shadow: 0 0 18px #0284c7; margin: 0 auto; display: flex; align-items: center; justify-content: center;">' +
|
||||||
|
'<span style="font-size: 15px;">🚗</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>',
|
||||||
|
iconSize: [140, 55],
|
||||||
|
iconAnchor: [70, 52]
|
||||||
});
|
});
|
||||||
activePolylines.push(polyAlt1, polyAlt2);
|
}
|
||||||
|
|
||||||
var allPts = greenPts.concat(redPts).concat(alt1Pts).concat(alt2Pts);
|
function animateMovingVehicle(routePoints, vehicle) {
|
||||||
if (allPts.length > 0) {
|
if (vehicleAnimationTimer) {
|
||||||
var startPt = greenPts[0] || allPts[0];
|
clearInterval(vehicleAnimationTimer);
|
||||||
var endPt = redPts.length > 0 ? redPts[redPts.length - 1] : (greenPts[greenPts.length - 1] || allPts[allPts.length - 1]);
|
vehicleAnimationTimer = null;
|
||||||
var mStart = new google.maps.Marker({ position: {lat: startPt[0], lng: startPt[1]}, map: map, title: "Başlangıç Noktası" });
|
|
||||||
var mEnd = new google.maps.Marker({ position: {lat: endPt[0], lng: endPt[1]}, map: map, title: vehicle.destination });
|
|
||||||
activeMarkers.push(mStart, mEnd);
|
|
||||||
|
|
||||||
var bounds = new google.maps.LatLngBounds();
|
|
||||||
allPts.forEach(function(p) { bounds.extend({lat: p[0], lng: p[1]}); });
|
|
||||||
map.fitBounds(bounds);
|
|
||||||
}
|
}
|
||||||
|
if (!routePoints || routePoints.length === 0) return;
|
||||||
|
|
||||||
} else {
|
// Canlı Takip Modu: Arabayı her zaman rotanın EN SON (en güncel) noktasına koy.
|
||||||
// Leaflet.js Görsel Çizimler (Uydu Temalı)
|
var lastPoint = routePoints[routePoints.length - 1];
|
||||||
|
var speedStr = vehicle.gps_speed ? vehicle.gps_speed + " km/s" : "0 km/s";
|
||||||
|
|
||||||
// 1. Alternatif Gri Yollar
|
var staticCar = L.marker(lastPoint, {
|
||||||
var polyAlt1 = L.polyline(alt1Pts, { color: '#64748b', weight: 4, opacity: 0.6, dashArray: '8, 6' }).addTo(map);
|
icon: createLiveCarIcon(vehicle.driver_tag, speedStr)
|
||||||
var polyAlt2 = L.polyline(alt2Pts, { color: '#64748b', weight: 4, opacity: 0.6, dashArray: '8, 6' }).addTo(map);
|
}).addTo(map);
|
||||||
activePolylines.push(polyAlt1, polyAlt2);
|
activeMarkers.push(staticCar);
|
||||||
|
|
||||||
|
// Haritayı doğrudan bu son noktaya odakla (Zoom)
|
||||||
|
map.setView(lastPoint, 16);
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Yeşil Ana Rota Çizgisi
|
function selectDriver(vehicle) {
|
||||||
if (greenPts.length > 0) {
|
if (vehicleAnimationTimer) {
|
||||||
var polyGreen = L.polyline(greenPts, { color: '#10b981', weight: 8, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }).addTo(map);
|
clearInterval(vehicleAnimationTimer);
|
||||||
activePolylines.push(polyGreen);
|
vehicleAnimationTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Kırmızı 1 km+ Sapma Çizgisi
|
if (isGoogleLoaded) {
|
||||||
if (redPts.length > 0) {
|
activePolylines.forEach(function(p) { p.setMap(null); });
|
||||||
var polyRed = L.polyline(redPts, { color: '#ef4444', weight: 8, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }).addTo(map);
|
activeMarkers.forEach(function(m) { m.setMap(null); });
|
||||||
activePolylines.push(polyRed);
|
} else {
|
||||||
|
activePolylines.forEach(function(p) { map.removeLayer(p); });
|
||||||
|
activeMarkers.forEach(function(m) { map.removeLayer(m); });
|
||||||
}
|
}
|
||||||
|
activePolylines = [];
|
||||||
|
activeMarkers = [];
|
||||||
|
|
||||||
var allPts = greenPts.concat(redPts);
|
var greenPts = vehicle.green_route || [];
|
||||||
// Başlangıç ve Bitiş İğnelerini (Mavi/Kırmızı) Canlı Radarda Gizliyoruz (Sadece araba ve çizgi görünecek)
|
var redPts = vehicle.red_route || [];
|
||||||
// Kırmızı Yol Üzerindeki "1.5 km Sapma" Çağrı Kutucuğu
|
var alt1Pts = vehicle.alt1 || [];
|
||||||
|
var alt2Pts = vehicle.alt2 || [];
|
||||||
|
|
||||||
|
if (isGoogleLoaded) {
|
||||||
|
if (greenPts.length > 0) {
|
||||||
|
var polyGreen = new google.maps.Polyline({
|
||||||
|
path: greenPts.map(function(p) { return {lat: p[0], lng: p[1]}; }),
|
||||||
|
strokeColor: '#10b981',
|
||||||
|
strokeOpacity: 0.95,
|
||||||
|
strokeWeight: 7,
|
||||||
|
map: map
|
||||||
|
});
|
||||||
|
activePolylines.push(polyGreen);
|
||||||
|
}
|
||||||
if (redPts.length > 0) {
|
if (redPts.length > 0) {
|
||||||
var midRedIdx = Math.floor(redPts.length / 2);
|
var polyRed = new google.maps.Polyline({
|
||||||
var mRedCallout = L.marker(redPts[midRedIdx], { icon: createCalloutIcon(vehicle.deviation_label, '#ef4444') }).addTo(map);
|
path: redPts.map(function(p) { return {lat: p[0], lng: p[1]}; }),
|
||||||
activeMarkers.push(mRedCallout);
|
strokeColor: '#ef4444',
|
||||||
|
strokeOpacity: 0.95,
|
||||||
|
strokeWeight: 7,
|
||||||
|
map: map
|
||||||
|
});
|
||||||
|
activePolylines.push(polyRed);
|
||||||
|
}
|
||||||
|
var polyAlt1 = new google.maps.Polyline({
|
||||||
|
path: alt1Pts.map(function(p) { return {lat: p[0], lng: p[1]}; }),
|
||||||
|
strokeColor: '#64748b',
|
||||||
|
strokeOpacity: 0.55,
|
||||||
|
strokeWeight: 4,
|
||||||
|
map: map
|
||||||
|
});
|
||||||
|
var polyAlt2 = new google.maps.Polyline({
|
||||||
|
path: alt2Pts.map(function(p) { return {lat: p[0], lng: p[1]}; }),
|
||||||
|
strokeColor: '#64748b',
|
||||||
|
strokeOpacity: 0.55,
|
||||||
|
strokeWeight: 4,
|
||||||
|
map: map
|
||||||
|
});
|
||||||
|
activePolylines.push(polyAlt1, polyAlt2);
|
||||||
|
|
||||||
|
var allPts = greenPts.concat(redPts).concat(alt1Pts).concat(alt2Pts);
|
||||||
|
if (allPts.length > 0) {
|
||||||
|
var startPt = greenPts[0] || allPts[0];
|
||||||
|
var endPt = redPts.length > 0 ? redPts[redPts.length - 1] : (greenPts[greenPts.length - 1] || allPts[allPts.length - 1]);
|
||||||
|
|
||||||
|
var bounds = new google.maps.LatLngBounds();
|
||||||
|
allPts.forEach(function(p) { bounds.extend({lat: p[0], lng: p[1]}); });
|
||||||
|
map.fitBounds(bounds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rotada Gerçek Zamanlı Canlı Hareket Eden Araç Animasyonunu Başlat
|
} else {
|
||||||
animateMovingVehicle(allPts, vehicle);
|
// Leaflet.js Görsel Çizimler (Uydu Temalı)
|
||||||
|
|
||||||
|
// 1. Alternatif Gri Yollar
|
||||||
|
var polyAlt1 = L.polyline(alt1Pts, { color: '#64748b', weight: 4, opacity: 0.6, dashArray: '8, 6' }).addTo(map);
|
||||||
|
var polyAlt2 = L.polyline(alt2Pts, { color: '#64748b', weight: 4, opacity: 0.6, dashArray: '8, 6' }).addTo(map);
|
||||||
|
activePolylines.push(polyAlt1, polyAlt2);
|
||||||
|
|
||||||
var group = new L.featureGroup([polyAlt1, polyAlt2].concat(activePolylines));
|
// 2. Yeşil Ana Rota Çizgisi
|
||||||
map.fitBounds(group.getBounds(), { padding: [50, 50] });
|
if (greenPts.length > 0) {
|
||||||
|
var polyGreen = L.polyline(greenPts, { color: '#10b981', weight: 8, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }).addTo(map);
|
||||||
|
activePolylines.push(polyGreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Kırmızı 1 km+ Sapma Çizgisi
|
||||||
|
if (redPts.length > 0) {
|
||||||
|
var polyRed = L.polyline(redPts, { color: '#ef4444', weight: 8, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }).addTo(map);
|
||||||
|
activePolylines.push(polyRed);
|
||||||
|
}
|
||||||
|
|
||||||
|
var allPts = greenPts.concat(redPts);
|
||||||
|
if (allPts.length > 0) {
|
||||||
|
var startPt = greenPts[0] || allPts[0];
|
||||||
|
var endPt = redPts.length > 0 ? redPts[redPts.length - 1] : (greenPts[greenPts.length - 1] || allPts[allPts.length - 1]);
|
||||||
|
|
||||||
|
// Başlangıç ve Bitiş İğnelerini (Mavi/Kırmızı) Canlı Radarda Gizliyoruz (Sadece araba ve çizgi görünecek)
|
||||||
|
// Kırmızı Yol Üzerindeki "1.5 km Sapma" Çağrı Kutucuğu
|
||||||
|
if (redPts.length > 0) {
|
||||||
|
var midRedIdx = Math.floor(redPts.length / 2);
|
||||||
|
var mRedCallout = L.marker(redPts[midRedIdx], { icon: createCalloutIcon(vehicle.deviation_label, '#ef4444') }).addTo(map);
|
||||||
|
activeMarkers.push(mRedCallout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotada Gerçek Zamanlı Canlı Hareket Eden Araç Animasyonunu Başlat
|
||||||
|
animateMovingVehicle(allPts, vehicle);
|
||||||
|
|
||||||
|
var group = new L.featureGroup([polyAlt1, polyAlt2].concat(activePolylines));
|
||||||
|
map.fitBounds(group.getBounds(), { padding: [50, 50] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yüzen Detay Kartını Güncelle
|
||||||
|
var overlayEl = document.getElementById('driver_info_overlay');
|
||||||
|
if (overlayEl) {
|
||||||
|
var badgeBg = vehicle.is_violation ? 'rgba(239, 68, 68, 0.25)' : (vehicle.is_manager_approved ? 'rgba(56, 189, 248, 0.25)' : 'rgba(16, 185, 129, 0.25)');
|
||||||
|
var badgeBorder = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981');
|
||||||
|
var badgeText = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981');
|
||||||
|
var badgeTitle = vehicle.is_violation
|
||||||
|
? vehicle.badge_violation
|
||||||
|
: (vehicle.is_manager_approved ? vehicle.badge_approved : vehicle.badge_ok);
|
||||||
|
|
||||||
|
overlayEl.style.display = 'block';
|
||||||
|
overlayEl.innerHTML =
|
||||||
|
'<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">' +
|
||||||
|
'<div>' +
|
||||||
|
'<h3 style="color: white; font-size: 15px; font-weight: 700; margin: 0;">' + (vehicle.driver_tag_str || '') + ' - ' + vehicle.driver_name + '</h3>' +
|
||||||
|
'<span style="color: #94a3b8; font-size: 12px;">' + vehicle.license_plate + ' (' + vehicle.name + ')</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<span style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; color: ' + badgeText + '; font-size: 11px; font-weight: bold; padding: 4px 8px; border-radius: 12px;">' +
|
||||||
|
vehicle.label_score + ' ' + vehicle.score +
|
||||||
|
'</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; border-radius: 8px; padding: 8px; margin-bottom: 12px; font-size: 11px; font-weight: bold; color: ' + badgeText + '; text-align: center;">' +
|
||||||
|
badgeTitle +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="font-size: 12px; line-height: 1.6; color: #cbd5e1;">' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Personel Evi / Konaklama:</span>' +
|
||||||
|
'<strong style="color: #f1f5f9; font-size: 11px;">' + (vehicle.driver_home_address || 'Bilinmiyor') + '</strong>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Başlangıç Noktası:</span>' +
|
||||||
|
'<strong style="color: white; font-size: 11px;">' + (vehicle.start_location || 'Canlı GPS Konumu') + '</strong>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Ulaşım Hedefi:</span>' +
|
||||||
|
'<strong style="color: #38bdf8; font-size: 11px;">' + (vehicle.destination || 'Gerçek Zamanlı Aktif İzleme') + '</strong>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Planlanan / Gidilen Mesafe:</span>' +
|
||||||
|
'<span>' + vehicle.planned_km + ' KM / <strong>' + vehicle.driven_km + ' KM</strong></span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Rota Sapma Durumu:</span>' +
|
||||||
|
'<strong style="color: ' + (vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981')) + ';">' + vehicle.deviation_label + '</strong>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
||||||
|
'<span style="color: #94a3b8;">Şirket Politikası:</span>' +
|
||||||
|
'<span style="color: ' + (vehicle.has_free_use ? '#38bdf8' : '#f59e0b') + '; font-weight: 600;">' + vehicle.policy_desc + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yüzen Detay Kartını Güncelle
|
var tableBody = document.getElementById('vehicleTableBody');
|
||||||
var overlayEl = document.getElementById('driver_info_overlay');
|
if (tableBody) {
|
||||||
if (overlayEl) {
|
tableBody.innerHTML = '';
|
||||||
var badgeBg = vehicle.is_violation ? 'rgba(239, 68, 68, 0.25)' : (vehicle.is_manager_approved ? 'rgba(56, 189, 248, 0.25)' : 'rgba(16, 185, 129, 0.25)');
|
var movingCount = 0;
|
||||||
var badgeBorder = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981');
|
var parkedCount = 0;
|
||||||
var badgeText = vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981');
|
var offlineCount = 0;
|
||||||
var badgeTitle = vehicle.is_violation
|
|
||||||
? vehicle.badge_violation
|
|
||||||
: (vehicle.is_manager_approved ? vehicle.badge_approved : vehicle.badge_ok);
|
|
||||||
|
|
||||||
overlayEl.style.display = 'block';
|
vehiclesData.forEach(function(v, idx) {
|
||||||
overlayEl.innerHTML =
|
// Sunucudan (Odoo JSON dosyası) gelen rotayı ezici olarak kullan
|
||||||
'<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">' +
|
if (serverHistory && serverHistory.length > 0) {
|
||||||
'<div>' +
|
v.green_route = serverHistory;
|
||||||
'<h3 style="color: white; font-size: 15px; font-weight: 700; margin: 0;">' + (vehicle.driver_tag_str || '') + ' - ' + vehicle.driver_name + '</h3>' +
|
}
|
||||||
'<span style="color: #94a3b8; font-size: 12px;">' + vehicle.license_plate + ' (' + vehicle.name + ')</span>' +
|
|
||||||
'</div>' +
|
|
||||||
'<span style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; color: ' + badgeText + '; font-size: 11px; font-weight: bold; padding: 4px 8px; border-radius: 12px;">' +
|
|
||||||
vehicle.label_score + ' ' + vehicle.score +
|
|
||||||
'</span>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div style="background: ' + badgeBg + '; border: 1px solid ' + badgeBorder + '; border-radius: 8px; padding: 8px; margin-bottom: 12px; font-size: 11px; font-weight: bold; color: ' + badgeText + '; text-align: center;">' +
|
|
||||||
badgeTitle +
|
|
||||||
'</div>' +
|
|
||||||
'<div style="font-size: 12px; line-height: 1.6; color: #cbd5e1;">' +
|
|
||||||
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
|
||||||
'<span style="color: #94a3b8;">Personel Evi / Konaklama:</span>' +
|
|
||||||
'<strong style="color: #f1f5f9; font-size: 11px;">' + (vehicle.driver_home_address || 'Bilinmiyor') + '</strong>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
|
||||||
'<span style="color: #94a3b8;">Başlangıç Noktası:</span>' +
|
|
||||||
'<strong style="color: white; font-size: 11px;">' + (vehicle.start_location || 'Canlı GPS Konumu') + '</strong>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
|
||||||
'<span style="color: #94a3b8;">Ulaşım Hedefi:</span>' +
|
|
||||||
'<strong style="color: #38bdf8; font-size: 11px;">' + (vehicle.destination || 'Gerçek Zamanlı Aktif İzleme') + '</strong>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
|
||||||
'<span style="color: #94a3b8;">Planlanan / Gidilen Mesafe:</span>' +
|
|
||||||
'<span>' + vehicle.planned_km + ' KM / <strong>' + vehicle.driven_km + ' KM</strong></span>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
|
||||||
'<span style="color: #94a3b8;">Rota Sapma Durumu:</span>' +
|
|
||||||
'<strong style="color: ' + (vehicle.is_violation ? '#ef4444' : (vehicle.is_manager_approved ? '#38bdf8' : '#10b981')) + ';">' + vehicle.deviation_label + '</strong>' +
|
|
||||||
'</div>' +
|
|
||||||
'<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">' +
|
|
||||||
'<span style="color: #94a3b8;">Şirket Politikası:</span>' +
|
|
||||||
'<span style="color: ' + (vehicle.has_free_use ? '#38bdf8' : '#f59e0b') + '; font-weight: 600;">' + vehicle.policy_desc + '</span>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var tableBody = document.getElementById('vehicleTableBody');
|
// Basit bir kural: Hızı 0'dan büyük olanlar HAREKETLİ, 0 olanlar PARK, veri olmayanlar ÇEVRİMDIŞI.
|
||||||
if (tableBody) {
|
var speedStr = "0 km/s";
|
||||||
tableBody.innerHTML = '';
|
if (v.gps_speed !== undefined) {
|
||||||
var movingCount = 0;
|
if (v.gps_speed > 0) {
|
||||||
var parkedCount = 0;
|
movingCount++;
|
||||||
var offlineCount = 0;
|
speedStr = v.gps_speed + " km/s";
|
||||||
|
} else {
|
||||||
|
parkedCount++;
|
||||||
|
speedStr = "0 km/s";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Test verilerinde gps_speed olmayabilir, fake data üzerinden simüle et
|
||||||
|
var randomStatus = Math.random();
|
||||||
|
if (randomStatus > 0.4) { movingCount++; speedStr = Math.floor(Math.random() * 80 + 20) + " km/s"; }
|
||||||
|
else if (randomStatus > 0.1) { parkedCount++; speedStr = "0 km/s"; }
|
||||||
|
else { offlineCount++; speedStr = "Offline"; }
|
||||||
|
}
|
||||||
|
|
||||||
vehiclesData.forEach(function(v, idx) {
|
var tr = document.createElement('tr');
|
||||||
// === Tarayıcı Tabanlı Rota Çizgisi Geçmişi (Frontend History) ===
|
tr.style.cursor = 'pointer';
|
||||||
var historyKey = 'mymach_route_' + v.id;
|
tr.style.transition = 'all 0.2s ease';
|
||||||
var history = [];
|
tr.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
|
||||||
try {
|
|
||||||
history = JSON.parse(localStorage.getItem(historyKey) || '[]');
|
var isRev = v.is_violation;
|
||||||
} catch(e) {}
|
var badgeStyle = isRev
|
||||||
|
? 'background: rgba(239, 68, 68, 0.2); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.4);'
|
||||||
|
: (v.is_manager_approved
|
||||||
|
? 'background: rgba(56, 189, 248, 0.2); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.4);'
|
||||||
|
: 'background: rgba(16, 185, 129, 0.2); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.4);');
|
||||||
|
var statusLabel = isRev ? (v.badge_violation) : (v.is_manager_approved ? v.badge_approved : v.badge_ok);
|
||||||
|
|
||||||
|
tr.innerHTML =
|
||||||
|
'<td style="padding: 10px;">' +
|
||||||
|
'<strong style="color: white; font-size: 13px;">' + (v.driver_tag_str || '') + ' - ' + v.driver_name + '</strong><br/>' +
|
||||||
|
'<span style="color: #94a3b8; font-size: 11px;">' + v.license_plate + '</span>' +
|
||||||
|
'</td>' +
|
||||||
|
'<td style="padding: 10px;">' +
|
||||||
|
'<span style="font-size: 13px; font-weight: 700; color: #38bdf8;">' + speedStr + '</span>' +
|
||||||
|
'</td>' +
|
||||||
|
'<td style="padding: 10px;">' +
|
||||||
|
'<span style="' + badgeStyle + ' padding: 4px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; display: inline-block;">' +
|
||||||
|
statusLabel +
|
||||||
|
'</span>' +
|
||||||
|
'</td>';
|
||||||
|
|
||||||
|
tr.addEventListener('click', function() {
|
||||||
|
var children = tableBody.children;
|
||||||
|
for (var i = 0; i < children.length; i++) {
|
||||||
|
children[i].style.background = 'transparent';
|
||||||
|
}
|
||||||
|
tr.style.background = 'rgba(56, 189, 248, 0.2)';
|
||||||
|
selectDriver(v);
|
||||||
|
});
|
||||||
|
|
||||||
|
tableBody.appendChild(tr);
|
||||||
|
|
||||||
|
if (idx === 0) {
|
||||||
|
tr.style.background = 'rgba(56, 189, 248, 0.2)';
|
||||||
|
setTimeout(function() { selectDriver(v); }, 300);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metrikleri DOM'a yaz
|
||||||
|
var mMoving = document.getElementById('metric_moving');
|
||||||
|
if (mMoving) mMoving.textContent = movingCount;
|
||||||
|
|
||||||
if (v.green_route && v.green_route.length > 0) {
|
var mParked = document.getElementById('metric_parked');
|
||||||
var currentLat = v.green_route[0][0];
|
if (mParked) mParked.textContent = parkedCount;
|
||||||
var currentLon = v.green_route[0][1];
|
|
||||||
|
var mOffline = document.getElementById('metric_offline');
|
||||||
var addPoint = true;
|
if (mOffline) mOffline.textContent = offlineCount;
|
||||||
if (history.length > 0) {
|
}
|
||||||
var lastPt = history[history.length - 1];
|
|
||||||
// Sadece BİREBİR aynı koordinatsa ekleme. (Hafif bir GPS oynamasını bile kaydetmek için)
|
var miniChartEl = document.getElementById('miniChart');
|
||||||
if (lastPt[0] === currentLat && lastPt[1] === currentLon) {
|
if (miniChartEl) {
|
||||||
addPoint = false;
|
var ctx = miniChartEl.getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: ['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'KM Trend',
|
||||||
|
data: [2200, 2500, 2100, 2800, 3100, 2700],
|
||||||
|
borderColor: '#00f2fe',
|
||||||
|
backgroundColor: 'rgba(0, 242, 254, 0.05)',
|
||||||
|
borderWidth: 2,
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: { legend: { display: false } },
|
||||||
|
scales: {
|
||||||
|
x: { grid: { display: false }, ticks: { color: '#94a3b8' } },
|
||||||
|
y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (addPoint) {
|
|
||||||
history.push([currentLat, currentLon]);
|
|
||||||
if (history.length > 300) history = history.slice(-300); // 300 noktayı hafızada tut
|
|
||||||
localStorage.setItem(historyKey, JSON.stringify(history));
|
|
||||||
}
|
|
||||||
v.green_route = history;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basit bir kural: Hızı 0'dan büyük olanlar HAREKETLİ, 0 olanlar PARK, veri olmayanlar ÇEVRİMDIŞI.
|
|
||||||
var speedStr = "0 km/s";
|
|
||||||
if (v.gps_speed !== undefined) {
|
|
||||||
if (v.gps_speed > 0) {
|
|
||||||
movingCount++;
|
|
||||||
speedStr = v.gps_speed + " km/s";
|
|
||||||
} else {
|
|
||||||
parkedCount++;
|
|
||||||
speedStr = "0 km/s";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Test verilerinde gps_speed olmayabilir, fake data üzerinden simüle et
|
|
||||||
var randomStatus = Math.random();
|
|
||||||
if (randomStatus > 0.4) { movingCount++; speedStr = Math.floor(Math.random() * 80 + 20) + " km/s"; }
|
|
||||||
else if (randomStatus > 0.1) { parkedCount++; speedStr = "0 km/s"; }
|
|
||||||
else { offlineCount++; speedStr = "Offline"; }
|
|
||||||
}
|
|
||||||
|
|
||||||
var tr = document.createElement('tr');
|
|
||||||
tr.style.cursor = 'pointer';
|
|
||||||
tr.style.transition = 'all 0.2s ease';
|
|
||||||
tr.style.borderBottom = '1px solid rgba(255,255,255,0.05)';
|
|
||||||
|
|
||||||
var isRev = v.is_violation;
|
|
||||||
var badgeStyle = isRev
|
|
||||||
? 'background: rgba(239, 68, 68, 0.2); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.4);'
|
|
||||||
: (v.is_manager_approved
|
|
||||||
? 'background: rgba(56, 189, 248, 0.2); color: #38bdf8; border: 1px solid rgba(56, 189, 248, 0.4);'
|
|
||||||
: 'background: rgba(16, 185, 129, 0.2); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.4);');
|
|
||||||
var statusLabel = isRev ? (v.badge_violation) : (v.is_manager_approved ? v.badge_approved : v.badge_ok);
|
|
||||||
|
|
||||||
tr.innerHTML =
|
|
||||||
'<td style="padding: 10px;">' +
|
|
||||||
'<strong style="color: white; font-size: 13px;">' + (v.driver_tag_str || '') + ' - ' + v.driver_name + '</strong><br/>' +
|
|
||||||
'<span style="color: #94a3b8; font-size: 11px;">' + v.license_plate + '</span>' +
|
|
||||||
'</td>' +
|
|
||||||
'<td style="padding: 10px;">' +
|
|
||||||
'<span style="font-size: 13px; font-weight: 700; color: #38bdf8;">' + speedStr + '</span>' +
|
|
||||||
'</td>' +
|
|
||||||
'<td style="padding: 10px;">' +
|
|
||||||
'<span style="' + badgeStyle + ' padding: 4px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; display: inline-block;">' +
|
|
||||||
statusLabel +
|
|
||||||
'</span>' +
|
|
||||||
'</td>';
|
|
||||||
|
|
||||||
tr.addEventListener('click', function() {
|
|
||||||
var children = tableBody.children;
|
|
||||||
for (var i = 0; i < children.length; i++) {
|
|
||||||
children[i].style.background = 'transparent';
|
|
||||||
}
|
|
||||||
tr.style.background = 'rgba(56, 189, 248, 0.2)';
|
|
||||||
selectDriver(v);
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
tableBody.appendChild(tr);
|
|
||||||
|
|
||||||
if (idx === 0) {
|
|
||||||
tr.style.background = 'rgba(56, 189, 248, 0.2)';
|
|
||||||
setTimeout(function() { selectDriver(v); }, 300);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Metrikleri DOM'a yaz
|
|
||||||
var mMoving = document.getElementById('metric_moving');
|
|
||||||
if (mMoving) mMoving.textContent = movingCount;
|
|
||||||
|
|
||||||
var mParked = document.getElementById('metric_parked');
|
// --- 10 Saniyede Bir Sayfayı Tam Ekran Yenileyerek Görüntüyü Güncelleyen Kod ---
|
||||||
if (mParked) mParked.textContent = parkedCount;
|
setInterval(function() {
|
||||||
|
window.location.reload();
|
||||||
var mOffline = document.getElementById('metric_offline');
|
}, 15000);
|
||||||
if (mOffline) mOffline.textContent = offlineCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
var miniChartEl = document.getElementById('miniChart');
|
|
||||||
if (miniChartEl) {
|
|
||||||
var ctx = miniChartEl.getContext('2d');
|
|
||||||
new Chart(ctx, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: ['Oca', 'Şub', 'Mar', 'Nis', 'May', 'Haz'],
|
|
||||||
datasets: [{
|
|
||||||
label: 'KM Trend',
|
|
||||||
data: [2200, 2500, 2100, 2800, 3100, 2700],
|
|
||||||
borderColor: '#00f2fe',
|
|
||||||
backgroundColor: 'rgba(0, 242, 254, 0.05)',
|
|
||||||
borderWidth: 2,
|
|
||||||
tension: 0.4,
|
|
||||||
fill: true
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
plugins: { legend: { display: false } },
|
|
||||||
scales: {
|
|
||||||
x: { grid: { display: false }, ticks: { color: '#94a3b8' } },
|
|
||||||
y: { grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#94a3b8' } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user