update: admin scan
parent
b9431a67b1
commit
6080584ac5
|
@ -25,4 +25,11 @@ public class AdminController : Controller
|
|||
{
|
||||
return View("~/Views/Admin/Transport/SpjAdmin/History/Index.cshtml");
|
||||
}
|
||||
|
||||
[HttpGet("history/details/{id}")]
|
||||
public IActionResult Details(int id)
|
||||
{
|
||||
ViewData["Id"] = id;
|
||||
return View("~/Views/Admin/Transport/SpjAdmin/History/Details.cshtml");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,8 @@
|
|||
<i class="w-5 h-5 text-orange-600" data-lucide="file-text"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-bold text-gray-900">Data SPJ</h2>
|
||||
<p class="text-xs text-gray-500">Surat Perintah Jalan</p>
|
||||
<h2 class="text-lg font-bold text-gray-900">Muhammad Yusuf</h2>
|
||||
<p class="text-xs text-gray-500">Data SPJ</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
<div class="max-w-sm mx-auto bg-gray-50 min-h-screen">
|
||||
<div class="bg-gradient-to-r from-orange-500 to-orange-600 text-white px-4 py-4 sticky top-0 z-10 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<a href="@Url.Action("Index", "Home")" class="p-2 hover:bg-white/10 rounded-full transition-all duration-200">
|
||||
<a href="@Url.Action("Index", "Admin")" class="p-2 hover:bg-white/10 rounded-full transition-all duration-200">
|
||||
<i class="w-5 h-5" data-lucide="chevron-left"></i>
|
||||
</a>
|
||||
<h1 class="text-lg font-bold">Riwayat Perjalanan</h1>
|
||||
<h1 class="text-lg font-bold">Riwayat SPJ</h1>
|
||||
<div class="w-9"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,6 +19,7 @@
|
|||
{
|
||||
new {
|
||||
Id = 1,
|
||||
Nama = "Yusuf",
|
||||
NoSpj = "SPJ/07-2025/PKM/000478",
|
||||
Plat = "B 5678 ABC",
|
||||
Kode = "JRC 007",
|
||||
|
@ -29,6 +30,7 @@
|
|||
},
|
||||
new {
|
||||
Id = 2,
|
||||
Nama = "Ahmad Rizki",
|
||||
NoSpj = "SPJ/07-2025/PKM/000476",
|
||||
Plat = "B 9632 TOR",
|
||||
Kode = "JRC 005",
|
||||
|
@ -39,6 +41,7 @@
|
|||
},
|
||||
new {
|
||||
Id = 3,
|
||||
Nama = "Siti Aminah",
|
||||
NoSpj = "SPJ/07-2025/PKM/000477",
|
||||
Plat = "B 1234 XYZ",
|
||||
Kode = "JRC 006",
|
||||
|
@ -49,6 +52,7 @@
|
|||
},
|
||||
new {
|
||||
Id = 4,
|
||||
Nama = "Budi Santoso",
|
||||
NoSpj = "SPJ/07-2025/PKM/000479",
|
||||
Plat = "B 9876 DEF",
|
||||
Kode = "JRC 008",
|
||||
|
@ -59,6 +63,7 @@
|
|||
},
|
||||
new {
|
||||
Id = 5,
|
||||
Nama = "Dewi Lestari",
|
||||
NoSpj = "SPJ/07-2025/PKM/000480",
|
||||
Plat = "B 4321 GHI",
|
||||
Kode = "JRC 009",
|
||||
|
@ -73,15 +78,15 @@
|
|||
<div class="px-4 py-4 space-y-3">
|
||||
@foreach (var spj in spjList)
|
||||
{
|
||||
<a href="@Url.Action("Details", "History", new { id = spj.Id })" class="block">
|
||||
<a href="@Url.Action("Details", "Admin", new { id = spj.Id })" class="block">
|
||||
<div class="bg-white rounded-2xl p-4 shadow-sm border border-gray-100 hover:shadow-lg hover:border-orange-200 transition-all duration-300 relative overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-orange-400 to-orange-500"></div>
|
||||
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<div class="w-2 h-2 bg-orange-400 rounded-full"></div>
|
||||
<span class="text-xs font-medium text-gray-500 uppercase tracking-wider">No. SPJ</span>
|
||||
<i class="w-4 h-4 text-green-600" data-lucide="user"></i>
|
||||
<span class="text-xs font-medium text-gray-500 uppercase tracking-wider">@spj.Nama</span>
|
||||
</div>
|
||||
<div class="font-bold text-gray-900 text-sm">@spj.NoSpj</div>
|
||||
</div>
|
||||
|
|
|
@ -1,220 +1,183 @@
|
|||
@{
|
||||
Layout = "~/Views/Admin/Transport/SpjAdmin/Shared/_Layout.cshtml";
|
||||
ViewData["Title"] = "Home Page";
|
||||
ViewData["Title"] = "Admin Dashboard";
|
||||
}
|
||||
|
||||
<div class="container max-w-sm mx-auto bg-white min-h-screen">
|
||||
<div class="container max-w-sm mx-auto bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen relative overflow-hidden">
|
||||
|
||||
<div class="absolute top-0 max-w-sm container mx-auto bg-orange-500 text-white rounded-br-[125px] h-[250px] flex flex-row justify-between items-start px-6 py-6 shadow-lg z-20">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-md font-bold leading-tight text-white">Bonny Agung Putra</h1>
|
||||
<p class="text-xs opacity-90 font-medium text-orange-100">Driver UPST</span></p>
|
||||
<div class="mt-5 flex items-center gap-2">
|
||||
<i class="w-4 h-4 text-white" data-lucide="map-pin"></i>
|
||||
<span class="text-sm opacity-90">Lokasi Anda:</span>
|
||||
</div>
|
||||
<p id="userLocation" class="font-semibold text-xs tracking-wide cursor-pointer underline text-white hover:text-orange-200 transition">
|
||||
Mendeteksi lokasi...
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="relative flex flex-col items-center justify-center">
|
||||
<div class="w-12 h-12 rounded-full border-3 border-white overflow-hidden shadow-md flex items-center justify-center cursor-pointer group" id="profileMenuButton">
|
||||
<i class="w-8 h-8 text-white" data-lucide="user"></i>
|
||||
</div>
|
||||
<div id="profileMenuDropdown" class="absolute top-12 right-0 mt-2 w-32 bg-white rounded shadow-lg py-2 z-50 hidden">
|
||||
<form method="post" asp-controller="Auth" asp-action="Logout">
|
||||
<button type="submit" class="hover:cursor-pointer flex items-center gap-2 w-full text-left px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50 transition rounded-md">
|
||||
<i class="w-4 h-4" data-lucide="log-out"></i>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 mx-4 px-6 py-12 mt-40 rounded-3xl relative overflow-hidden shadow-2xl z-21 border border-slate-200">
|
||||
<div class="absolute top-0 left-0 w-full h-full pointer-events-none">
|
||||
<div class="absolute top-4 right-8 w-6 h-6 bg-orange-300 rounded-full opacity-60 animate-bounce" style="animation-delay: 0.5s;"></div>
|
||||
<div class="absolute top-12 left-12 w-4 h-4 bg-blue-400 rounded-full opacity-40 animate-pulse" style="animation-delay: 1s;"></div>
|
||||
<div class="absolute bottom-8 left-8 w-5 h-5 bg-indigo-300 rounded-full opacity-50 animate-bounce" style="animation-delay: 1.5s;"></div>
|
||||
<div class="absolute bottom-16 right-16 w-3 h-3 bg-slate-400 rounded-full opacity-30 animate-pulse" style="animation-delay: 2s;"></div>
|
||||
</div>
|
||||
<div class="relative z-10">
|
||||
<div class="bg-gradient-to-br from-orange-500 via-orange-600 to-red-500 rounded-b-[2rem] shadow-2xl p-6 mb-6">
|
||||
|
||||
<div class="relative z-10 text-center">
|
||||
<div class="w-24 h-24 mx-auto mb-6 relative">
|
||||
<div class="w-full h-full bg-gradient-to-br from-orange-400 to-orange-600 rounded-full flex items-center justify-center shadow-xl animate-pulse">
|
||||
<div class="w-20 h-20 bg-white rounded-full flex items-center justify-center">
|
||||
<i class="w-10 h-10 text-orange-500" data-lucide="clipboard-list"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 border-4 border-orange-300 rounded-full animate-ping opacity-30"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 pt-8 pb-4">
|
||||
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-xl font-bold bg-gradient-to-r from-slate-700 to-slate-900 bg-clip-text text-transparent">
|
||||
Belum Ada SPJ
|
||||
</h2>
|
||||
<p class="text-sm text-slate-600 leading-relaxed px-4 mb-2">
|
||||
Anda belum memiliki <span class="font-semibold text-orange-600">Surat Perintah Jalan</span> yang aktif saat ini.
|
||||
</p>
|
||||
</div>
|
||||
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-white/20 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="relative">
|
||||
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-white to-orange-100 p-1 shadow-lg">
|
||||
<div class="w-full h-full rounded-xl bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center shadow-inner">
|
||||
<i class="w-8 h-8 text-white" data-lucide="user"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute -bottom-1 -right-1 w-5 h-5 bg-green-400 rounded-full border-2 border-white shadow-sm">
|
||||
<div class="w-full h-full bg-green-500 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/70 backdrop-blur-sm border border-white/30 rounded-2xl p-5 mb-6 text-left">
|
||||
<div class="space-y-2 text-xs text-slate-600">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-1 h-1 bg-orange-400 rounded-full mt-1.5 flex-shrink-0"></div>
|
||||
<p>SPJ akan diterbitkan oleh admin sesuai jadwal kerja</p>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-bold text-white leading-tight">Yusuf</h1>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="bg-orange-700/30 text-orange-100 px-3 py-1 rounded-full text-sm font-base backdrop-blur-sm border border-orange-400/20">
|
||||
Administrator
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-1 h-1 bg-orange-400 rounded-full mt-1.5 flex-shrink-0"></div>
|
||||
<p>Periksa koneksi internet dan aktifkan lokasi GPS</p>
|
||||
|
||||
<div class="relative">
|
||||
<button id="profileMenuButton" class="w-12 h-12 rounded-xl bg-white/20 backdrop-blur-sm border border-white/30 flex items-center justify-center hover:bg-white/30 transition-all duration-300 group shadow-lg hover:shadow-xl hover:scale-105">
|
||||
<i class="w-5 h-5 text-white group-hover:rotate-180 transition-transform duration-300" data-lucide="settings"></i>
|
||||
</button>
|
||||
|
||||
<div id="profileMenuDropdown" class="absolute top-14 right-0 w-48 bg-white rounded-xl shadow-2xl py-2 z-50 hidden border border-gray-100 overflow-hidden">
|
||||
<div class="px-4 py-3 border-b border-gray-100">
|
||||
<p class="text-sm font-semibold text-gray-900">Yusuf</p>
|
||||
<p class="text-xs text-gray-500">Administrator</p>
|
||||
</div>
|
||||
|
||||
<div class="py-1">
|
||||
<div class="border-t border-gray-100 mt-1"></div>
|
||||
<form method="post" asp-controller="Auth" asp-action="Logout">
|
||||
<button type="submit" class="flex items-center gap-3 w-full text-left px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50 transition-colors">
|
||||
<i class="w-4 h-4 text-red-500" data-lucide="log-out"></i>
|
||||
<span>Logout</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="refreshButton" class="bg-gradient-to-r from-orange-500 to-orange-600 hover:from-orange-600 hover:to-orange-700 text-white px-6 py-3 rounded-2xl text-sm font-bold shadow-lg hover:shadow-xl transition-all duration-300 flex items-center gap-2 mx-auto transform hover:scale-105">
|
||||
<i class="w-4 h-4" data-lucide="refresh-cw" id="refreshIcon"></i>
|
||||
<span id="refreshText">Refresh Halaman</span>
|
||||
</button>
|
||||
<div class="mt-4 text-center">
|
||||
<h2 class="text-white/90 text-lg font-semibold">Selamat Datang!</h2>
|
||||
<p class="text-orange-100/70 text-sm">Siap kelola sistem eSPJ dengan efisien</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 mb-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="bg-white rounded-xl p-4 shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs font-medium">Total Driver</p>
|
||||
<p class="text-2xl font-bold text-gray-900">10</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center">
|
||||
<i class="w-5 h-5 text-green-600" data-lucide="users"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-500 text-xs font-medium">Total SPJ</p>
|
||||
<p class="text-2xl font-bold text-gray-900">15</p>
|
||||
</div>
|
||||
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center">
|
||||
<i class="w-5 h-5 text-blue-600" data-lucide="map"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="~/Views/Admin/Transport/SpjAdmin/Shared/Components/_Navigation.cshtml" />
|
||||
|
||||
</div>
|
||||
|
||||
<partial name="~/Views/Admin/Transport/SpjAdmin/Shared/Components/_Navigation.cshtml" />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<register-block dynamic-section="scripts" key="jsHomeKosong">
|
||||
<register-block dynamic-section="scripts" key="jsHomeAdmin">
|
||||
|
||||
<style>
|
||||
@@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@@keyframes shimmer {
|
||||
0% { background-position: -200px 0; }
|
||||
100% { background-position: calc(200px + 100%) 0; }
|
||||
0%, 100% { transform: translateY(0) rotate(0deg); }
|
||||
50% { transform: translateY(-10px) rotate(2deg); }
|
||||
}
|
||||
|
||||
.float-animation {
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.shimmer {
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
|
||||
background-size: 200px 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.refresh-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
.float-animation-delayed {
|
||||
animation: float 4s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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();
|
||||
});
|
||||
|
||||
const refreshBtn = document.getElementById('refreshButton');
|
||||
const refreshIcon = document.getElementById('refreshIcon');
|
||||
const refreshText = document.getElementById('refreshText');
|
||||
|
||||
if (refreshBtn) {
|
||||
refreshBtn.addEventListener("click", function() {
|
||||
refreshIcon.style.animation = 'spin 1s linear infinite';
|
||||
refreshText.textContent = 'Memuat...';
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.style.opacity = '0.8';
|
||||
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
const mainIcon = document.querySelector('[data-lucide="clipboard-list"]');
|
||||
if (mainIcon) {
|
||||
mainIcon.closest('.w-24').classList.add('float-animation');
|
||||
}
|
||||
|
||||
const mainCard = document.querySelector('.bg-gradient-to-br');
|
||||
if (mainCard) {
|
||||
mainCard.style.opacity = '0';
|
||||
mainCard.style.transform = 'translateY(20px)';
|
||||
mainCard.style.transition = 'all 0.6s ease-out';
|
||||
|
||||
setTimeout(() => {
|
||||
mainCard.style.opacity = '1';
|
||||
mainCard.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
</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");
|
||||
});
|
||||
if (btn && dropdown) {
|
||||
btn.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (dropdown.classList.contains("hidden")) {
|
||||
dropdown.classList.remove("hidden");
|
||||
dropdown.style.opacity = "0";
|
||||
dropdown.style.transform = "translateY(-10px) scale(0.95)";
|
||||
|
||||
setTimeout(() => {
|
||||
dropdown.style.transition = "all 0.2s ease-out";
|
||||
dropdown.style.opacity = "1";
|
||||
dropdown.style.transform = "translateY(0) scale(1)";
|
||||
}, 10);
|
||||
} else {
|
||||
dropdown.style.transition = "all 0.15s ease-in";
|
||||
dropdown.style.opacity = "0";
|
||||
dropdown.style.transform = "translateY(-10px) scale(0.95)";
|
||||
|
||||
setTimeout(() => {
|
||||
dropdown.classList.add("hidden");
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", function () {
|
||||
dropdown.classList.add("hidden");
|
||||
document.addEventListener("click", function (e) {
|
||||
if (!btn.contains(e.target) && !dropdown.contains(e.target)) {
|
||||
dropdown.style.transition = "all 0.15s ease-in";
|
||||
dropdown.style.opacity = "0";
|
||||
dropdown.style.transform = "translateY(-10px) scale(0.95)";
|
||||
|
||||
setTimeout(() => {
|
||||
dropdown.classList.add("hidden");
|
||||
}, 150);
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
const statCards = document.querySelectorAll('.grid > div');
|
||||
statCards.forEach((card, index) => {
|
||||
card.style.opacity = "0";
|
||||
card.style.transform = "translateY(20px)";
|
||||
|
||||
setTimeout(() => {
|
||||
card.style.transition = "all 0.3s ease-out";
|
||||
card.style.opacity = "1";
|
||||
card.style.transform = "translateY(0)";
|
||||
}, 300 + (index * 100));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -93,26 +93,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm">
|
||||
<div class="flex items-start">
|
||||
<i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i>
|
||||
<div class="text-gray-700">
|
||||
<p class="font-medium mb-1">Tips Scanning:</p>
|
||||
<ul class="text-xs space-y-1">
|
||||
<li>• Pastikan QR code dalam pencahayaan yang cukup</li>
|
||||
<li>• Jaga jarak 15-30cm dari kamera</li>
|
||||
<li>• Arahkan kamera secara tegak lurus ke QR code</li>
|
||||
<li>• Pastikan QR code tidak buram atau rusak</li>
|
||||
<li>• <strong>Klik "Izinkan/Allow" saat browser meminta akses kamera</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t pt-4">
|
||||
<h3 class="text-gray-700 font-medium mb-3">Atau input manual:</h3>
|
||||
<form id="manual-form" method="post" action="@Url.Action("ProcessScan", "Scan")">
|
||||
@Html.AntiForgeryToken()
|
||||
<div class="flex gap-2">
|
||||
<input type="text"
|
||||
id="manual-barcode"
|
||||
|
@ -152,7 +138,23 @@
|
|||
<partial name="~/Views/Admin/Transport/SpjAdmin/Shared/Components/_Navigation.cshtml" />
|
||||
</div>
|
||||
|
||||
|
||||
<div id="scan-modal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden flex items-center justify-center">
|
||||
<div class="bg-white py-4 rounded-2xl shadow-2xl max-w-sm w-full border border-gray-100">
|
||||
<div class="p-8 text-center">
|
||||
<div id="modal-icon" class="mx-auto mb-6">
|
||||
</div>
|
||||
<h3 id="modal-title" class="text-xl font-bold mb-3 text-gray-800"></h3>
|
||||
<p id="modal-message" class="text-gray-600 mb-6 leading-relaxed"></p>
|
||||
<button id="modal-close" class="bg-orange-500 hover:bg-orange-600 text-white px-8 py-3 rounded-xl transition-all duration-200 font-medium shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<register-block dynamic-section="scripts" key="jsScan">
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js" type="text/javascript"></script>
|
||||
|
||||
<script>
|
||||
|
@ -196,6 +198,13 @@
|
|||
this.confirmBtn.addEventListener('click', () => this.confirmScan());
|
||||
this.retryBtn.addEventListener('click', () => this.retryScan());
|
||||
this.manualForm.addEventListener('submit', (e) => this.handleManualSubmit(e));
|
||||
|
||||
$('#modal-close').on('click', () => this.hideModal());
|
||||
$('#scan-modal').on('click', (e) => {
|
||||
if (e.target.id === 'scan-modal') {
|
||||
this.hideModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
checkBrowserSupport() {
|
||||
|
@ -326,13 +335,13 @@
|
|||
handleBarcodeDetected(decodedText, decodedResult) {
|
||||
if (decodedText && decodedText.length >= 5) {
|
||||
this.flashSuccess();
|
||||
this.playSuccessSound();
|
||||
this.vibrate();
|
||||
|
||||
this.detectedCode = decodedText;
|
||||
this.showResult(decodedText);
|
||||
this.stopScanner();
|
||||
this.playSuccessSound();
|
||||
this.stopScanner();
|
||||
|
||||
this.vibrate();
|
||||
this.processScanCode(decodedText);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,11 +377,46 @@
|
|||
|
||||
confirmScan() {
|
||||
if (this.detectedCode) {
|
||||
this.manualInput.value = this.detectedCode;
|
||||
this.manualForm.submit();
|
||||
this.processScanCode(this.detectedCode);
|
||||
}
|
||||
}
|
||||
|
||||
processScanCode(code) {
|
||||
this.showModal('loading', 'Memproses...', 'Sedang memverifikasi kode SPJ...', false);
|
||||
|
||||
$.ajax({
|
||||
url: '@Url.Action("ProcessScan", "Scan")', // nanti tinggal ganti aja yaa
|
||||
type: 'POST',
|
||||
data: {
|
||||
barcode: code
|
||||
},
|
||||
success: (response) => {
|
||||
if (response.success) {
|
||||
this.showModal('success', 'Scan Berhasil!', 'SPJ berhasil ditemukan dan diproses.', true);
|
||||
setTimeout(() => {
|
||||
this.hideResult();
|
||||
this.manualInput.value = '';
|
||||
}, 2000);
|
||||
} else {
|
||||
this.showModal('error', 'SPJ Tidak Ditemukan', response.message || 'Kode SPJ tidak ditemukan dalam database.', true);
|
||||
}
|
||||
},
|
||||
error: (xhr, status, error) => {
|
||||
let errorMessage = 'Terjadi kesalahan saat memproses scan.';
|
||||
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
} else if (xhr.status === 404) {
|
||||
errorMessage = 'SPJ tidak ditemukan dalam database.';
|
||||
} else if (xhr.status === 500) {
|
||||
errorMessage = 'Terjadi kesalahan server. Silakan coba lagi.';
|
||||
}
|
||||
|
||||
this.showModal('error', 'Error', errorMessage, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async retryScan() {
|
||||
this.hideResult();
|
||||
this.hideError();
|
||||
|
@ -389,18 +433,20 @@
|
|||
}
|
||||
|
||||
handleManualSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const code = this.manualInput.value.trim();
|
||||
if (!code) {
|
||||
e.preventDefault();
|
||||
this.showError('Silakan masukkan kode SPJ.');
|
||||
this.showModal('error', 'Input Kosong', 'Silakan masukkan kode SPJ.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (code.length < 5) {
|
||||
e.preventDefault();
|
||||
this.showError('Kode SPJ minimal 5 karakter.');
|
||||
this.showModal('error', 'Kode Tidak Valid', 'Kode SPJ minimal 5 karakter.', true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.processScanCode(code);
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
|
@ -454,9 +500,54 @@
|
|||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
showModal(type, title, message, showCloseButton = true) {
|
||||
const iconHtml = {
|
||||
'success': '<div class="w-20 h-20 mx-auto bg-green-100 rounded-full flex items-center justify-center shadow-lg"><i class="w-10 h-10 text-green-600" data-lucide="check-circle"></i></div>',
|
||||
'error': '<div class="w-20 h-20 mx-auto bg-red-100 rounded-full flex items-center justify-center shadow-lg"><i class="w-10 h-10 text-red-600" data-lucide="x-circle"></i></div>',
|
||||
'loading': '<div class="w-20 h-20 mx-auto bg-blue-100 rounded-full flex items-center justify-center shadow-lg"><div class="loading-spinner-modal"></div></div>'
|
||||
};
|
||||
|
||||
$('#modal-icon').html(iconHtml[type] || iconHtml['error']);
|
||||
$('#modal-title').text(title);
|
||||
$('#modal-message').text(message);
|
||||
|
||||
if (showCloseButton) {
|
||||
$('#modal-close').show();
|
||||
} else {
|
||||
$('#modal-close').hide();
|
||||
}
|
||||
|
||||
$('#scan-modal').removeClass('hidden');
|
||||
|
||||
$('#scan-modal').css('opacity', '0').animate({'opacity': '1'}, 300);
|
||||
$('#scan-modal .bg-white').css('transform', 'scale(0.8)').animate({
|
||||
'transform': 'scale(1)'
|
||||
}, 300);
|
||||
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
hideModal() {
|
||||
$('#scan-modal').animate({'opacity': '0'}, 200, function() {
|
||||
$('#scan-modal').addClass('hidden');
|
||||
$('#scan-modal').css('opacity', '');
|
||||
$('#scan-modal .bg-white').css('transform', '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function(xhr, settings) {
|
||||
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
|
||||
xhr.setRequestHeader("RequestVerificationToken", $('input[name="__RequestVerificationToken"]').val());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function waitForLibrary() {
|
||||
if (typeof Html5Qrcode !== 'undefined') {
|
||||
new BarcodeScanner();
|
||||
|
|
|
@ -86,6 +86,15 @@
|
|||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.loading-spinner-modal {
|
||||
border: 3px solid rgba(59, 130, 246, 0.3);
|
||||
border-top: 3px solid #3b82f6;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue