945 lines
41 KiB
Plaintext
945 lines
41 KiB
Plaintext
@{
|
|
Layout = "~/Views/Admin/Transport/SpjDriverUpst/Shared/_Layout.cshtml";
|
|
ViewData["Title"] = "Home Page";
|
|
}
|
|
|
|
<div class="w-full lg:max-w-sm mx-auto bg-gray-50 min-h-screen pb-28 relative font-sans">
|
|
|
|
<div class="bg-upst text-white px-6 pt-10 pb-24 rounded-b-[50px] shadow-2xl relative overflow-hidden">
|
|
<img src="@Url.Content("~/driver/upst_white.svg")" alt="UPST Logo" class="absolute top-4 left-6 w-20 h-auto opacity-20">
|
|
<div class="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -mr-16 -mt-16"></div>
|
|
|
|
<div class="flex justify-between items-start relative z-10">
|
|
<div class="space-y-1">
|
|
<p class="text-[10px] pt-5 font-black uppercase tracking-[0.2em] text-orange-200 opacity-80">Akun Driver</p>
|
|
<h1 class="text-2xl font-extrabold tracking-tight">Bonny Agung</h1>
|
|
<div class="flex items-center gap-2 mt-2 bg-black/20 w-fit px-3 py-1 rounded-full border border-white/10">
|
|
<span class="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
|
|
<span class="text-[10px] font-bold uppercase" id="currentDateTime">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="profileMenuButton" class="w-12 h-12 rounded-2xl bg-white/10 border border-white/20 backdrop-blur-lg flex items-center justify-center shadow-lg transition-transform active:scale-90">
|
|
<i class="w-6 h-6 text-white" data-lucide="user"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div id="profileMenuDropdown" class="absolute top-24 right-6 w-36 bg-white rounded-2xl shadow-2xl py-2 z-50 hidden border border-gray-100">
|
|
<form method="post" asp-controller="Auth" asp-action="Logout">
|
|
<button type="submit" class="flex items-center gap-3 w-full px-4 py-3 text-xs font-bold text-red-600 hover:bg-red-50 transition-colors">
|
|
<i class="w-4 h-4" data-lucide="log-out"></i> Logout
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-4 -mt-16 relative z-20 flex flex-col gap-6">
|
|
|
|
<div class="bg-upst-light rounded-[28px] p-4 border border-gray-100 shadow-sm">
|
|
<div class="flex items-center gap-4 group cursor-pointer" id="userLocationBtn">
|
|
<div class="w-12 h-12 bg-upst rounded-2xl flex items-center justify-center shrink-0 shadow-md group-active:scale-90 transition-transform">
|
|
<i class="w-6 h-6 text-white" data-lucide="map-pin"></i>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-1">Lokasi Saat Ini</p>
|
|
<p id="userLocation" class="text-sm font-bold text-gray-800 leading-tight line-clamp-2">Mendeteksi lokasi...</p>
|
|
</div>
|
|
<div class="w-10 h-10 rounded-full bg-white flex items-center justify-center shadow-sm border border-gray-100 shrink-0 group-active:rotate-180 transition-transform duration-500">
|
|
<i class="w-4 h-4 text-gray-500" data-lucide="refresh-cw"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col gap-4">
|
|
|
|
<div class="bg-white p-1.5 rounded-2xl border border-gray-100 shadow-sm flex">
|
|
<button id="tabSpjButton" type="button" class="flex-1 py-3 text-xs font-black rounded-xl bg-upst text-white shadow-md transition-all">
|
|
DETAIL SPJ
|
|
</button>
|
|
<button id="tabMapsButton" type="button" class="flex-1 py-3 text-xs font-black rounded-xl text-gray-500 bg-transparent hover:bg-gray-50 transition-all">
|
|
LIVE MAPS
|
|
</button>
|
|
</div>
|
|
|
|
<div class="relative">
|
|
|
|
<div id="spjCardPanel" class="bg-upst rounded-[32px] overflow-hidden shadow-xl flex flex-col h-[340px]">
|
|
|
|
<div class="px-6 py-5 bg-black/10 border-b border-white/5 flex justify-between items-center shrink-0">
|
|
<div>
|
|
<p class="text-[10px] font-black text-green-100 uppercase tracking-widest mb-1">Nomor SPJ</p>
|
|
<p class="text-white font-mono font-bold text-sm">SPJ/07-2025/PKM/000476</p>
|
|
</div>
|
|
<div class="bg-white px-3 py-1.5 rounded-xl shadow-sm">
|
|
<span class="text-green-600 text-xs font-black">B 9632 TOR</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-6 flex-1 flex flex-col justify-center relative overflow-hidden">
|
|
<img src="@Url.Content("~/driver/tree.svg")" class="absolute -right-6 -bottom-6 w-40 h-40 opacity-10 pointer-events-none" alt="">
|
|
|
|
<div class="z-10 flex flex-row justify-between items-center w-full mt-2">
|
|
<div class="flex-1 pr-4">
|
|
<p class="text-[10px] text-green-100 font-bold uppercase tracking-wider mb-2">Tujuan Pembuangan</p>
|
|
<h2 class="text-3xl font-black text-white leading-tight mb-2">JRC Rorotan</h2>
|
|
<span class="bg-white/10 text-green-50 text-xs font-bold px-3 py-1 rounded-lg border border-white/10 inline-block">
|
|
JRC 005
|
|
</span>
|
|
</div>
|
|
|
|
<button id="qrCodeTrigger" class="bg-white p-4 rounded-3xl shadow-xl border-4 border-white/20 active:scale-90 transition-transform group hover:border-green-400 shrink-0">
|
|
<img src="@Url.Content("~/driver/images/qr.png")" alt="QR" class="w-10 h-10 object-contain group-hover:scale-110 transition-transform">
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div id="mapsCardPanel" class="bg-upst rounded-[32px] overflow-hidden shadow-xl hidden flex-col h-[340px]">
|
|
|
|
<div class="px-6 py-5 bg-black/10 flex justify-between items-center shrink-0 z-20">
|
|
<div>
|
|
<p class="text-[10px] font-black text-green-100 uppercase tracking-widest mb-1">Rute Pengangkutan</p>
|
|
<p class="text-white font-bold text-sm">Live Location</p>
|
|
</div>
|
|
<div class="bg-white/10 border border-white/20 px-3 py-1.5 rounded-xl shadow-sm flex items-center gap-2">
|
|
<span class="w-2 h-2 rounded-full bg-red-400 animate-pulse"></span>
|
|
<span class="text-white text-[10px] font-black tracking-wider">LIVE</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="relative flex-1 w-full bg-gray-100">
|
|
<div id="pickupMap" class="absolute inset-0 w-full h-full z-10"></div>
|
|
|
|
<button id="recenterMapButton" type="button" class="absolute bottom-5 right-5 z-20 w-12 h-12 bg-white text-upst rounded-2xl shadow-lg border border-gray-200 flex items-center justify-center active:scale-90 transition-transform">
|
|
<i class="w-6 h-6" data-lucide="locate-fixed"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="px-6 mt-10">
|
|
<div class="flex justify-between items-end mb-6">
|
|
<div>
|
|
<h3 class="text-xl font-black text-gray-800 tracking-tighter">Lokasi</h3>
|
|
<p class="text-[10px] text-gray-400 font-bold uppercase">Pengangkutan</p>
|
|
</div>
|
|
<button id="addPickupButton" class="bg-upst text-white p-3 rounded-2xl shadow-lg shadow-gray-200 hover:brightness-110 active:scale-90 transition-all">
|
|
<i class="w-5 h-5" data-lucide="plus"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-6 relative">
|
|
<div class="absolute left-6 top-2 bottom-2 w-0.5 bg-gray-200 rounded-full"></div>
|
|
|
|
<a href="@Url.Action("Index", "DetailPenjemputan")" class="block relative pl-12 group">
|
|
<div class="absolute left-4 top-1 w-4 h-4 bg-white border-4 border-gray-400 rounded-full z-10"></div>
|
|
<div class="bg-white p-4 rounded-3xl border border-gray-100 shadow-sm group-active:bg-gray-50 transition-colors">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<span class="text-[9px] font-black text-gray-400 uppercase tracking-widest">Proses</span>
|
|
<i class="w-4 h-4 text-gray-300" data-lucide="loader"></i>
|
|
</div>
|
|
<h4 class="font-bold text-gray-800 text-sm leading-tight">CV Tri Mitra Utama - Shell Radio Dalam</h4>
|
|
<div class="flex items-center gap-1 mt-2 text-gray-500">
|
|
<i class="w-3 h-3" data-lucide="map"></i>
|
|
<p class="text-[10px] truncate italic">Jakarta Timur 13470</p>
|
|
</div>
|
|
<div class="mt-2">
|
|
<span class="text-[9px] font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded-lg">3 TPS</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
|
|
<a href="@Url.Action("TanpaTps", "DetailPenjemputan")" class="block relative pl-12 group">
|
|
<div class="absolute left-4 top-1 w-4 h-4 bg-white border-4 border-gray-400 rounded-full z-10"></div>
|
|
<div class="bg-white p-4 rounded-3xl border border-gray-100 shadow-sm group-active:bg-gray-50 transition-colors">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<span class="text-[9px] font-black text-gray-400 uppercase tracking-widest">Proses</span>
|
|
<i class="w-4 h-4 text-gray-300" data-lucide="loader"></i>
|
|
</div>
|
|
<h4 class="font-bold text-gray-800 text-sm leading-tight">CV Tri Berkah Sejahtera</h4>
|
|
<div class="flex items-center gap-1 mt-2 text-gray-500">
|
|
<i class="w-3 h-3" data-lucide="map"></i>
|
|
<p class="text-[10px] truncate italic">Duren Sawit, Jakarta Timur</p>
|
|
</div>
|
|
<div class="mt-2">
|
|
<span class="text-[9px] font-bold text-gray-600 bg-gray-100 px-2 py-1 rounded-lg">1 TPS</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
|
|
<a href="@Url.Action("DetailSelesai", "DetailPenjemputan")" class="block relative pl-12 group">
|
|
<div class="absolute left-4 top-1 w-4 h-4 bg-green-500 rounded-full z-10 ring-4 ring-green-100"></div>
|
|
<div class="bg-green-50/50 p-4 rounded-3xl border border-green-100 shadow-sm group-active:bg-green-100 transition-colors">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<span class="text-[9px] font-black text-green-600 uppercase italic tracking-tighter">Selesai</span>
|
|
<i class="w-4 h-4 text-green-500" data-lucide="check-circle-2"></i>
|
|
</div>
|
|
<h4 class="font-bold text-gray-800 text-sm leading-tight">CV Tri Mitra Utama - Shell Radio Dalam</h4>
|
|
<p class="text-[10px] text-green-700/60 mt-2">Jakarta Selatan</p>
|
|
<div class="mt-2">
|
|
<span class="text-[9px] font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded-lg">3 TPS</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
|
|
<a href="@Url.Action("DetailSelesaiTanpaTps", "DetailPenjemputan")" class="block relative pl-12 group">
|
|
<div class="absolute left-4 top-1 w-4 h-4 bg-green-500 rounded-full z-10 ring-4 ring-green-100"></div>
|
|
<div class="bg-green-50/50 p-4 rounded-3xl border border-green-100 shadow-sm group-active:bg-green-100 transition-colors">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<span class="text-[9px] font-black text-green-600 uppercase italic tracking-tighter">Selesai</span>
|
|
<i class="w-4 h-4 text-green-500" data-lucide="check-circle-2"></i>
|
|
</div>
|
|
<h4 class="font-bold text-gray-800 text-sm leading-tight">CV Tri Berkah Sejahtera</h4>
|
|
<p class="text-[10px] text-green-700/60 mt-2">Duren Sawit, Jakarta Timur</p>
|
|
<div class="mt-2">
|
|
<span class="text-[9px] font-bold text-gray-600 bg-gray-100 px-2 py-1 rounded-lg">1 TPS</span>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
|
|
<a href="@Url.Action("DetailBatal", "DetailPenjemputan")" class="block relative pl-12 group">
|
|
<div class="absolute left-4 top-1 w-4 h-4 bg-red-500 rounded-full z-10 ring-4 ring-red-100"></div>
|
|
<div class="bg-red-50/50 p-4 rounded-3xl border border-red-100 shadow-sm group-active:bg-red-100 transition-colors">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<span class="text-[9px] font-black text-red-600 uppercase italic tracking-tighter">Batal</span>
|
|
<i class="w-4 h-4 text-red-500" data-lucide="x-circle"></i>
|
|
</div>
|
|
<h4 class="font-bold text-gray-800 text-sm leading-tight">CV Tri Berkah Sejahtera</h4>
|
|
<p class="text-[10px] text-red-700/60 mt-2">Klender, Jakarta Timur</p>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<partial name="~/Views/Admin/Transport/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
|
|
</div>
|
|
|
|
<div id="qrCodeModal" class="fixed inset-0 bg-upst/90 backdrop-blur-xl items-center justify-center z-[100] hidden p-8 transition-all">
|
|
<div class="w-full max-w-xs space-y-6">
|
|
<div class="bg-white rounded-[45px] p-8 shadow-2xl relative overflow-hidden">
|
|
<div class="absolute top-0 left-0 w-full h-2 bg-upst-light"></div>
|
|
|
|
<div class="text-center mb-6">
|
|
<p class="text-[10px] font-black text-gray-400 uppercase tracking-[0.3em] mb-1">Scanner Siap</p>
|
|
<h3 class="text-lg font-black text-gray-800">Verifikasi SPJ</h3>
|
|
</div>
|
|
|
|
<div class="bg-gray-50 p-6 rounded-[35px] border-2 border-gray-100 shadow-inner">
|
|
<img src="@Url.Content("~/driver/images/qr.png")" alt="QR Code" class="w-full h-auto">
|
|
</div>
|
|
|
|
<div class="mt-6 text-center">
|
|
<p class="text-[11px] font-mono text-gray-500 break-all bg-gray-100 py-2 px-3 rounded-xl">SPJ/07-2025/PKM/000476</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="closeQrModal" class="w-full py-4 bg-white/10 border border-white/20 rounded-2xl text-white font-bold text-sm backdrop-blur-md active:scale-95 transition-all">
|
|
TUTUP
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="addPickupModal" class="fixed inset-0 bg-upst/90 backdrop-blur-xl items-center justify-center z-[100] hidden p-8 transition-all">
|
|
<div class="w-full max-w-xs space-y-6">
|
|
<div class="bg-white rounded-[45px] p-8 shadow-2xl relative overflow-hidden">
|
|
<div class="absolute top-0 left-0 w-full h-2 bg-upst-light"></div>
|
|
|
|
<div class="text-center mb-6">
|
|
<p class="text-[10px] font-black text-gray-400 uppercase tracking-[0.3em] mb-1">Tambah Lokasi</p>
|
|
<h3 class="text-lg font-black text-gray-800">Pengangkutan</h3>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="text-xs font-bold text-gray-600 uppercase tracking-wider">Pilih Lokasi Pengangkutan</label>
|
|
<select id="pickupSelect" class="w-full mt-2"></select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-6 text-center">
|
|
<button id="closeAddModal" class="w-full py-4 bg-upst text-white rounded-2xl font-bold text-sm active:scale-95 transition-all">
|
|
TUTUP
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<register-block dynamic-section="scripts" key="jsHomeIndex">
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const userLocationEl = document.getElementById("userLocation");
|
|
|
|
function reverseGeocode(lat, lng) {
|
|
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
const address = data.display_name || `${lat}, ${lng}`;
|
|
userLocationEl.textContent = address;
|
|
localStorage.setItem("user_latitude", lat);
|
|
localStorage.setItem("user_longitude", lng);
|
|
localStorage.setItem("user_address", address);
|
|
})
|
|
.catch(() => {
|
|
userLocationEl.textContent = `${lat}, ${lng}`;
|
|
});
|
|
}
|
|
|
|
function getLocationUpdate() {
|
|
if ("geolocation" in navigator) {
|
|
userLocationEl.textContent = "Mendeteksi lokasi baru...";
|
|
navigator.geolocation.getCurrentPosition(
|
|
function (position) {
|
|
const lat = position.coords.latitude.toFixed(6);
|
|
const lng = position.coords.longitude.toFixed(6);
|
|
reverseGeocode(lat, lng);
|
|
},
|
|
function () {
|
|
userLocationEl.textContent = "Lokasi tidak diizinkan";
|
|
}
|
|
);
|
|
} else {
|
|
userLocationEl.textContent = "Browser tidak mendukung lokasi";
|
|
}
|
|
}
|
|
|
|
const savedAddress = localStorage.getItem("user_address");
|
|
if (savedAddress) {
|
|
userLocationEl.textContent = savedAddress;
|
|
} else {
|
|
getLocationUpdate();
|
|
}
|
|
|
|
userLocationEl.addEventListener("click", function () {
|
|
getLocationUpdate();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const tabSpjButton = document.getElementById("tabSpjButton");
|
|
const tabMapsButton = document.getElementById("tabMapsButton");
|
|
const spjCardPanel = document.getElementById("spjCardPanel");
|
|
const mapsCardPanel = document.getElementById("mapsCardPanel");
|
|
|
|
if (!tabSpjButton || !tabMapsButton || !spjCardPanel || !mapsCardPanel) return;
|
|
|
|
function activateTab(tab) {
|
|
const isSpj = tab === "spj";
|
|
|
|
tabSpjButton.classList.toggle("bg-upst", isSpj);
|
|
tabSpjButton.classList.toggle("text-white", isSpj);
|
|
tabSpjButton.classList.toggle("bg-gray-100", !isSpj);
|
|
tabSpjButton.classList.toggle("text-gray-500", !isSpj);
|
|
|
|
tabMapsButton.classList.toggle("bg-upst", !isSpj);
|
|
tabMapsButton.classList.toggle("text-white", !isSpj);
|
|
tabMapsButton.classList.toggle("bg-gray-100", isSpj);
|
|
tabMapsButton.classList.toggle("text-gray-500", isSpj);
|
|
|
|
spjCardPanel.classList.toggle("hidden", !isSpj);
|
|
mapsCardPanel.classList.toggle("hidden", isSpj);
|
|
mapsCardPanel.classList.toggle("flex", !isSpj);
|
|
|
|
if (!isSpj) {
|
|
document.dispatchEvent(new CustomEvent("spj:show-maps"));
|
|
}
|
|
}
|
|
|
|
tabSpjButton.addEventListener("click", function () {
|
|
activateTab("spj");
|
|
});
|
|
|
|
tabMapsButton.addEventListener("click", function () {
|
|
activateTab("maps");
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const mapEl = document.getElementById("pickupMap");
|
|
const mapsCardPanel = document.getElementById("mapsCardPanel");
|
|
const recenterMapButton = document.getElementById("recenterMapButton");
|
|
if (!mapEl) return;
|
|
|
|
let mapInstance = null;
|
|
let mapBounds = null;
|
|
let mapInitialized = false;
|
|
|
|
let userMarker = null;
|
|
let liveLocationWatchId = null;
|
|
let lastUserLatLng = null;
|
|
let lastKnownHeading = 0;
|
|
|
|
function fitAllLocations() {
|
|
if (!mapInstance || !mapBounds) return;
|
|
mapInstance.invalidateSize();
|
|
if (Array.isArray(mapBounds) && mapBounds.length === 1) {
|
|
mapInstance.setView(mapBounds[0], 16);
|
|
return;
|
|
}
|
|
mapInstance.fitBounds(mapBounds, { padding: [24, 24], maxZoom: 17 });
|
|
}
|
|
|
|
function toRadians(deg) {
|
|
return deg * Math.PI / 180;
|
|
}
|
|
|
|
function toDegrees(rad) {
|
|
return rad * 180 / Math.PI;
|
|
}
|
|
|
|
function getBearing(from, to) {
|
|
const [lat1, lng1] = from;
|
|
const [lat2, lng2] = to;
|
|
|
|
const phi1 = toRadians(lat1);
|
|
const phi2 = toRadians(lat2);
|
|
const lambda1 = toRadians(lng1);
|
|
const lambda2 = toRadians(lng2);
|
|
|
|
const y = Math.sin(lambda2 - lambda1) * Math.cos(phi2);
|
|
const x =
|
|
Math.cos(phi1) * Math.sin(phi2) -
|
|
Math.sin(phi1) * Math.cos(phi2) * Math.cos(lambda2 - lambda1);
|
|
|
|
return (toDegrees(Math.atan2(y, x)) + 360) % 360;
|
|
}
|
|
|
|
function createTruckDivIcon(rotationDeg) {
|
|
const carIconHtml = `
|
|
<div class="w-10 h-10 flex items-center justify-center">
|
|
<img
|
|
src="@Url.Content("~/driver/images/truck_icon.png")"
|
|
alt="Truck"
|
|
class="w-10 h-10 object-contain drop-shadow-lg"
|
|
style="transform: rotate(${rotationDeg}deg); transition: transform 0.25s ease;"
|
|
/>
|
|
</div>`;
|
|
|
|
return L.divIcon({
|
|
className: 'bg-transparent border-0',
|
|
html: carIconHtml,
|
|
iconSize: [40, 40],
|
|
iconAnchor: [20, 20],
|
|
popupAnchor: [0, -15]
|
|
});
|
|
}
|
|
|
|
function handleUserPosition(position) {
|
|
if (!mapInstance) return;
|
|
|
|
const lat = position.coords.latitude;
|
|
const lng = position.coords.longitude;
|
|
const nextLatLng = [lat, lng];
|
|
|
|
let heading = Number.isFinite(position.coords.heading)
|
|
? position.coords.heading
|
|
: null;
|
|
|
|
if (!Number.isFinite(heading) && lastUserLatLng) {
|
|
heading = getBearing(lastUserLatLng, nextLatLng);
|
|
}
|
|
|
|
if (Number.isFinite(heading)) {
|
|
lastKnownHeading = heading;
|
|
}
|
|
|
|
const rotationDeg = (lastKnownHeading + 180) % 360;
|
|
const truckIcon = createTruckDivIcon(rotationDeg);
|
|
|
|
if (userMarker) {
|
|
userMarker.setLatLng(nextLatLng);
|
|
userMarker.setIcon(truckIcon);
|
|
} else {
|
|
userMarker = L.marker(nextLatLng, { icon: truckIcon, zIndexOffset: 1000 })
|
|
.addTo(mapInstance)
|
|
.bindPopup("<div class='text-xs font-bold text-center text-blue-600 mb-1'>Posisi Anda</div><div class='text-[10px] text-gray-500 text-center'>Diperbarui otomatis via GPS</div>");
|
|
}
|
|
|
|
lastUserLatLng = nextLatLng;
|
|
}
|
|
|
|
function updateUserLocationOnMap() {
|
|
if (!mapInstance || !("geolocation" in navigator)) return;
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
handleUserPosition,
|
|
function (error) {
|
|
console.warn("Gagal mengambil GPS untuk live tracking:", error);
|
|
},
|
|
{ enableHighAccuracy: true, maximumAge: 0, timeout: 15000 }
|
|
);
|
|
}
|
|
|
|
function startLiveTracking() {
|
|
if (!mapInstance || !("geolocation" in navigator) || liveLocationWatchId !== null) return;
|
|
|
|
liveLocationWatchId = navigator.geolocation.watchPosition(
|
|
handleUserPosition,
|
|
function (error) {
|
|
console.warn("Gagal mengambil GPS untuk live tracking:", error);
|
|
},
|
|
{ enableHighAccuracy: true, maximumAge: 0, timeout: 15000 }
|
|
);
|
|
}
|
|
|
|
function ensureLeaflet() {
|
|
return new Promise((resolve, reject) => {
|
|
if (window.L && typeof window.L.map === "function") {
|
|
resolve();
|
|
return;
|
|
}
|
|
if (!document.getElementById("leaflet-css")) {
|
|
const css = document.createElement("link");
|
|
css.id = "leaflet-css";
|
|
css.rel = "stylesheet";
|
|
css.href = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css";
|
|
document.head.appendChild(css);
|
|
}
|
|
const existingScript = document.getElementById("leaflet-js");
|
|
if (existingScript) {
|
|
existingScript.addEventListener("load", resolve, { once: true });
|
|
existingScript.addEventListener("error", reject, { once: true });
|
|
return;
|
|
}
|
|
const script = document.createElement("script");
|
|
script.id = "leaflet-js";
|
|
script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
document.body.appendChild(script);
|
|
});
|
|
}
|
|
|
|
function toNumber(value) {
|
|
const num = Number(value);
|
|
return Number.isFinite(num) ? num : null;
|
|
}
|
|
|
|
function isValidLatLng(lat, lng) {
|
|
return lat !== null && lng !== null && Math.abs(lat) <= 90 && Math.abs(lng) <= 180;
|
|
}
|
|
|
|
function normalizeLatLng(item) {
|
|
const lat = toNumber(item.latitude);
|
|
const lng = toNumber(item.longitude);
|
|
if (isValidLatLng(lat, lng)) return { lat, lng };
|
|
if (isValidLatLng(lng, lat)) return { lat: lng, lng: lat };
|
|
return null;
|
|
}
|
|
|
|
async function geocodeFromAddress(address) {
|
|
if (!address) return null;
|
|
try {
|
|
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}&limit=1`);
|
|
const data = await res.json();
|
|
if (!Array.isArray(data) || data.length === 0) return null;
|
|
const lat = toNumber(data[0].lat);
|
|
const lng = toNumber(data[0].lon);
|
|
return isValidLatLng(lat, lng) ? { lat, lng } : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function getCoordinates(item) {
|
|
const normalized = normalizeLatLng(item);
|
|
if (normalized) return normalized;
|
|
return geocodeFromAddress(item.alamat);
|
|
}
|
|
|
|
async function getRoadRouteLatLngs(latLngs) {
|
|
if (!Array.isArray(latLngs) || latLngs.length < 2) return null;
|
|
try {
|
|
const coordString = latLngs.map(([lat, lng]) => `${lng},${lat}`).join(";");
|
|
const url = `https://router.project-osrm.org/route/v1/driving/${coordString}?overview=full&geometries=geojson&steps=false&alternatives=false`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) return null;
|
|
const data = await res.json();
|
|
const coords = data?.routes?.[0]?.geometry?.coordinates;
|
|
if (!Array.isArray(coords) || coords.length < 2) return null;
|
|
return coords.map(pair => [toNumber(pair?.[1]), toNumber(pair?.[0])]).filter(([lat, lng]) => isValidLatLng(lat, lng));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function initMap() {
|
|
try {
|
|
if (mapInitialized && mapInstance) {
|
|
setTimeout(() => fitAllLocations(), 80);
|
|
return;
|
|
}
|
|
|
|
await ensureLeaflet();
|
|
|
|
const res = await fetch("@Url.Content("~/driver/json/pengangkutan.json")");
|
|
const payload = await res.json();
|
|
const rows = Array.isArray(payload?.data) ? payload.data : [];
|
|
|
|
const points = [];
|
|
for (const row of rows) {
|
|
const coords = await getCoordinates(row);
|
|
if (coords) points.push({ ...row, ...coords });
|
|
}
|
|
|
|
if (!points.length) {
|
|
mapEl.innerHTML = '<div class="h-full flex items-center justify-center text-xs font-semibold text-gray-500">Titik pengangkutan tidak ditemukan</div>';
|
|
return;
|
|
}
|
|
|
|
mapInstance = L.map("pickupMap", {
|
|
zoomControl: true,
|
|
attributionControl: true
|
|
});
|
|
|
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
maxZoom: 19,
|
|
attribution: "© OpenStreetMap"
|
|
}).addTo(mapInstance);
|
|
|
|
const pickupIcon = L.icon({
|
|
iconUrl: "@Url.Content("~/driver/images/loc2.svg")",
|
|
iconSize: [40, 40],
|
|
iconAnchor: [15, 30],
|
|
popupAnchor: [0, -28]
|
|
});
|
|
|
|
const latLngs = points.map(p => [p.lat, p.lng]);
|
|
|
|
points.forEach((point, index) => {
|
|
const popupContent = `
|
|
<div class="font-sans min-w-[150px]">
|
|
<b class="block text-sm text-gray-800 mb-1">${index + 1}. ${point.name || "Lokasi"}</b>
|
|
<span class="text-xs text-gray-500 block mb-3 line-clamp-2">${point.alamat || "-"}</span>
|
|
<a href="https://www.google.com/maps/dir/?api=1&destination=${point.lat},${point.lng}"
|
|
target="_blank"
|
|
class="bg-blue-600 !text-white w-full py-2 rounded-xl text-[11px] font-bold flex items-center justify-center shadow-md hover:bg-blue-700 active:scale-95 transition-all"
|
|
style="text-decoration:none;">
|
|
<svg class="w-3 h-3 mr-1.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/></svg> BUKA DI GMAPS
|
|
</a>
|
|
</div>
|
|
`;
|
|
|
|
L.marker([point.lat, point.lng], { icon: pickupIcon })
|
|
.addTo(mapInstance)
|
|
.bindPopup(popupContent);
|
|
});
|
|
|
|
let routeLatLngs = latLngs;
|
|
if (latLngs.length > 1) {
|
|
const routed = await getRoadRouteLatLngs(latLngs);
|
|
if (routed && routed.length > 1) {
|
|
routeLatLngs = routed;
|
|
}
|
|
|
|
L.polyline(routeLatLngs, {
|
|
color: "#0f2a3f",
|
|
weight: 4,
|
|
opacity: 0.9
|
|
}).addTo(mapInstance);
|
|
}
|
|
|
|
mapBounds = routeLatLngs;
|
|
fitAllLocations();
|
|
mapInitialized = true;
|
|
|
|
updateUserLocationOnMap();
|
|
startLiveTracking();
|
|
|
|
} catch (err) {
|
|
mapEl.innerHTML = '<div class="h-full flex items-center justify-center text-xs font-semibold text-red-500">Gagal memuat peta</div>';
|
|
console.error("Map init error:", err);
|
|
}
|
|
}
|
|
|
|
document.addEventListener("spj:show-maps", function () {
|
|
initMap();
|
|
});
|
|
|
|
if (recenterMapButton) {
|
|
recenterMapButton.addEventListener("click", async function () {
|
|
if (!mapInitialized || !mapInstance) {
|
|
await initMap();
|
|
return;
|
|
}
|
|
fitAllLocations();
|
|
});
|
|
}
|
|
|
|
if (mapsCardPanel && !mapsCardPanel.classList.contains("hidden")) {
|
|
initMap();
|
|
}
|
|
|
|
window.addEventListener("beforeunload", function () {
|
|
if (liveLocationWatchId !== null && "geolocation" in navigator) {
|
|
navigator.geolocation.clearWatch(liveLocationWatchId);
|
|
liveLocationWatchId = null;
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const btn = document.getElementById("profileMenuButton");
|
|
const dropdown = document.getElementById("profileMenuDropdown");
|
|
|
|
btn.addEventListener("click", function (e) {
|
|
e.stopPropagation();
|
|
dropdown.classList.toggle("hidden");
|
|
});
|
|
|
|
document.addEventListener("click", function () {
|
|
dropdown.classList.add("hidden");
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const qrTrigger = document.getElementById("qrCodeTrigger");
|
|
const qrModal = document.getElementById("qrCodeModal");
|
|
const closeModal = document.getElementById("closeQrModal");
|
|
let originalBrightness = null;
|
|
let wakeLock = null;
|
|
|
|
async function setMaxBrightness() {
|
|
try {
|
|
if ('wakeLock' in navigator) {
|
|
wakeLock = await navigator.wakeLock.request('screen');
|
|
}
|
|
|
|
if ('screen' in navigator && 'brightness' in navigator.screen) {
|
|
originalBrightness = navigator.screen.brightness;
|
|
navigator.screen.brightness = 1.0;
|
|
}
|
|
} catch (err) {
|
|
console.log('Brightness control not supported:', err);
|
|
}
|
|
}
|
|
|
|
async function restoreOriginalBrightness() {
|
|
try {
|
|
if (wakeLock) {
|
|
await wakeLock.release();
|
|
wakeLock = null;
|
|
}
|
|
|
|
if ('screen' in navigator && 'brightness' in navigator.screen && originalBrightness !== null) {
|
|
navigator.screen.brightness = originalBrightness;
|
|
}
|
|
} catch (err) {
|
|
console.log('Error restoring brightness:', err);
|
|
}
|
|
}
|
|
|
|
qrTrigger.addEventListener("click", function () {
|
|
qrModal.classList.remove("hidden");
|
|
qrModal.classList.add("flex");
|
|
setMaxBrightness();
|
|
|
|
if ('vibrate' in navigator) {
|
|
navigator.vibrate(50);
|
|
}
|
|
});
|
|
|
|
function closeQrCodeModal() {
|
|
qrModal.classList.add("hidden");
|
|
qrModal.classList.remove("flex");
|
|
restoreOriginalBrightness();
|
|
}
|
|
|
|
closeModal.addEventListener("click", closeQrCodeModal);
|
|
|
|
qrModal.addEventListener("click", function (e) {
|
|
if (e.target === qrModal) {
|
|
closeQrCodeModal();
|
|
}
|
|
});
|
|
|
|
document.addEventListener("keydown", function (e) {
|
|
if (e.key === "Escape" && !qrModal.classList.contains("hidden")) {
|
|
closeQrCodeModal();
|
|
}
|
|
});
|
|
|
|
document.addEventListener("visibilitychange", function () {
|
|
if (document.hidden && !qrModal.classList.contains("hidden")) {
|
|
restoreOriginalBrightness();
|
|
} else if (!document.hidden && !qrModal.classList.contains("hidden")) {
|
|
setMaxBrightness();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const addModal = document.getElementById("addPickupModal");
|
|
const closeAddModal = document.getElementById("closeAddModal");
|
|
const pickupSelect = document.getElementById("pickupSelect");
|
|
const plusButton = document.getElementById("addPickupButton");
|
|
|
|
function ensureSelect2() {
|
|
return new Promise((resolve, reject) => {
|
|
if (window.jQuery && window.jQuery.fn.select2) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
if (!document.getElementById("select2-css")) {
|
|
const css = document.createElement("link");
|
|
css.id = "select2-css";
|
|
css.rel = "stylesheet";
|
|
css.href = "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css";
|
|
document.head.appendChild(css);
|
|
}
|
|
|
|
const existingScript = document.getElementById("select2-js");
|
|
if (existingScript) {
|
|
existingScript.addEventListener("load", resolve, { once: true });
|
|
existingScript.addEventListener("error", reject, { once: true });
|
|
return;
|
|
}
|
|
|
|
const script = document.createElement("script");
|
|
script.id = "select2-js";
|
|
script.src = "https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js";
|
|
script.onload = resolve;
|
|
script.onerror = reject;
|
|
document.body.appendChild(script);
|
|
});
|
|
}
|
|
|
|
function calculateDistance(lat1, lon1, lat2, lon2) {
|
|
const R = 6371e3;
|
|
const φ1 = lat1 * Math.PI / 180;
|
|
const φ2 = lat2 * Math.PI / 180;
|
|
const Δφ = (lat2 - lat1) * Math.PI / 180;
|
|
const Δλ = (lon2 - lon1) * Math.PI / 180;
|
|
|
|
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
|
|
Math.cos(φ1) * Math.cos(φ2) *
|
|
Math.sin(Δλ/2) * Math.sin(Δλ/2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
|
|
return Math.round(R * c);
|
|
}
|
|
|
|
function getCurrentLocation() {
|
|
const lat = parseFloat(localStorage.getItem("user_latitude"));
|
|
const lng = parseFloat(localStorage.getItem("user_longitude"));
|
|
return { lat, lng };
|
|
}
|
|
|
|
async function initAddModal() {
|
|
try {
|
|
await ensureSelect2();
|
|
|
|
const res = await fetch("@Url.Content("~/driver/json/pickup_locations.json")");
|
|
const payload = await res.json();
|
|
const locations = Array.isArray(payload?.data) ? payload.data : [];
|
|
|
|
const current = getCurrentLocation();
|
|
const data = locations.map((loc, index) => {
|
|
let distanceText = '';
|
|
if (current.lat && current.lng && loc.latitude && loc.longitude) {
|
|
const distance = calculateDistance(current.lat, current.lng, loc.latitude, loc.longitude);
|
|
distanceText = ` (${distance} m)`;
|
|
}
|
|
return {
|
|
id: index,
|
|
text: `${loc.name} - ${loc.alamat}${distanceText}`,
|
|
lat: loc.latitude,
|
|
lng: loc.longitude,
|
|
name: loc.name,
|
|
alamat: loc.alamat,
|
|
distance: distanceText
|
|
};
|
|
});
|
|
|
|
$(pickupSelect).select2({
|
|
data: data,
|
|
placeholder: "Cari lokasi pengangkutan...",
|
|
allowClear: true,
|
|
width: '100%',
|
|
templateResult: function (item) {
|
|
if (!item.id) return item.text;
|
|
return $(`<div><strong>${item.name}</strong><br><small>${item.alamat}${item.distance}</small></div>`);
|
|
},
|
|
templateSelection: function (item) {
|
|
if (!item.id) return item.text;
|
|
return `${item.name} - ${item.alamat}${item.distance}`;
|
|
}
|
|
});
|
|
|
|
$(pickupSelect).on('select2:select', function (e) {
|
|
});
|
|
|
|
$(pickupSelect).on('select2:clear', function () {
|
|
});
|
|
|
|
} catch (err) {
|
|
console.error("Error initializing add modal:", err);
|
|
}
|
|
}
|
|
|
|
plusButton.addEventListener("click", function () {
|
|
addModal.classList.remove("hidden");
|
|
addModal.classList.add("flex");
|
|
initAddModal();
|
|
});
|
|
|
|
function closeAddPickupModal() {
|
|
addModal.classList.add("hidden");
|
|
addModal.classList.remove("flex");
|
|
$(pickupSelect).val(null).trigger('change');
|
|
}
|
|
|
|
closeAddModal.addEventListener("click", closeAddPickupModal);
|
|
|
|
addModal.addEventListener("click", function (e) {
|
|
if (e.target === addModal) {
|
|
closeAddPickupModal();
|
|
}
|
|
});
|
|
|
|
document.addEventListener("keydown", function (e) {
|
|
if (e.key === "Escape" && !addModal.classList.contains("hidden")) {
|
|
closeAddPickupModal();
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
function updateDateTime() {
|
|
const now = new Date();
|
|
const options = {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false
|
|
};
|
|
const formatted = now.toLocaleDateString('id-ID', options);
|
|
document.getElementById('currentDateTime').textContent = formatted;
|
|
}
|
|
updateDateTime();
|
|
setInterval(updateDateTime, 1000);
|
|
</script>
|
|
</register-block>
|