789 lines
33 KiB
Plaintext
789 lines
33 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">
|
|
<div class="bg-upst-light rounded-3xl p-4 border border-gray-100 mb-6">
|
|
<div class="flex items-center gap-4 group cursor-pointer" id="userLocationBtn">
|
|
<div class="w-10 h-10 bg-upst rounded-2xl flex items-center justify-center flex-shrink-0 group-active:scale-90 transition-transform">
|
|
<i class="w-5 h-5 text-white" data-lucide="map-pin"></i>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-[9px] font-black text-gray-400 uppercase tracking-wider leading-none mb-1">Lokasi Saat Ini</p>
|
|
<p id="userLocation" class="text-xs line-clamp-3 font-bold text-gray-700 italic">Mendeteksi lokasi...</p>
|
|
</div>
|
|
<i class="w-4 h-4 text-gray-700" data-lucide="refresh-cw"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-2 mb-4 p-1 bg-white rounded-2xl border border-gray-100 shadow-sm">
|
|
<button id="tabSpjButton" type="button" class="px-3 py-2 text-xs font-black rounded-xl bg-upst text-white transition-all">
|
|
SPJ
|
|
</button>
|
|
<button id="tabMapsButton" type="button" class="px-3 py-2 text-xs font-black rounded-xl text-gray-500 bg-gray-100 transition-all">
|
|
MAPS
|
|
</button>
|
|
</div>
|
|
|
|
<div id="spjCardPanel" class="bg-upst rounded-[35px] overflow-hidden shadow-2xl relative h-[300px]">
|
|
<div class="bg-white/10 backdrop-blur-md p-5 flex justify-between items-center border-b border-white/10">
|
|
<div>
|
|
<p class="text-[10px] font-black text-green-100 uppercase tracking-widest">Nomor SPJ</p>
|
|
<p class="text-white font-mono font-bold text-sm">SPJ/07-2025/PKM/000476</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="bg-white text-green-600 text-[10px] font-black px-3 py-1 rounded-lg shadow-sm">B 9632 TOR</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-6 relative h-[236px]">
|
|
<div class="flex justify-between items-center h-full">
|
|
<div class="space-y-4">
|
|
<div>
|
|
<p class="text-[10px] text-green-100 font-bold uppercase opacity-80">Tujuan Pembuangan</p>
|
|
<h2 class="text-2xl font-black text-white tracking-tight leading-tight">JRC Rorotan</h2>
|
|
<p class="text-xs text-green-100/70 font-medium">(JRC 005)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button id="qrCodeTrigger" class="w-16 h-16 bg-white rounded-[24px] flex items-center justify-center shadow-2xl border-4 border-green-600/20 active:scale-90 transition-transform">
|
|
<img src="@Url.Content("~/driver/images/qr.png")" alt="QR" class="w-10 h-10 object-contain">
|
|
</button>
|
|
</div>
|
|
|
|
<img src="@Url.Content("~/driver/tree.svg")" class="absolute -left-4 -bottom-4 w-20 h-20 opacity-10 pointer-events-none" alt="">
|
|
</div>
|
|
</div>
|
|
|
|
<div id="mapsCardPanel" class="bg-upst rounded-[35px] overflow-hidden shadow-2xl relative h-[300px] flex-col hidden">
|
|
<div class="bg-white/10 backdrop-blur-md p-5 flex justify-between items-center border-b border-white/10">
|
|
<div>
|
|
<p class="text-[10px] font-black text-green-100 uppercase tracking-widest">Maps</p>
|
|
<p class="text-white font-mono font-bold text-sm">Rute Pengangkutan</p>
|
|
</div>
|
|
<div class="text-right">
|
|
<span class="bg-white text-green-600 text-[10px] font-black px-3 py-1 rounded-lg shadow-sm">LIVE</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-6 relative h-[236px]">
|
|
<div id="pickupMap" class="h-full w-full z-10 rounded-3xl overflow-hidden border border-white/20"></div>
|
|
<button id="recenterMapButton" type="button" class="absolute top-9 right-9 z-20 w-11 h-11 bg-white/95 text-upst rounded-2xl shadow-xl border border-white/70 backdrop-blur flex items-center justify-center active:scale-95 transition-transform" title="Tampilkan semua lokasi">
|
|
<i class="w-5 h-5" data-lucide="locate-fixed"></i>
|
|
</button>
|
|
<img src="@Url.Content("~/driver/tree.svg")" class="absolute -left-4 -bottom-4 w-20 h-20 opacity-10 pointer-events-none" alt="">
|
|
</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">Ada 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-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 tracking-widest 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">Tidak ada 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 tracking-widest 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();
|
|
}
|
|
|
|
// Update Lokasi cuy
|
|
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;
|
|
|
|
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 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) => {
|
|
L.marker([point.lat, point.lng], { icon: pickupIcon })
|
|
.addTo(mapInstance)
|
|
.bindPopup(`<b>${index + 1}. ${point.name || "Lokasi"}</b><br>${point.alamat || "-"}`);
|
|
});
|
|
|
|
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;
|
|
|
|
setTimeout(() => {
|
|
fitAllLocations();
|
|
}, 80);
|
|
} 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();
|
|
}
|
|
});
|
|
</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>
|