Compare commits
6 Commits
b9431a67b1
...
9a3d8de8a6
Author | SHA1 | Date |
---|---|---|
|
9a3d8de8a6 | |
|
bed92f8ab0 | |
|
a677ad959e | |
|
c0e8aeccbc | |
|
5ef8ff38e8 | |
|
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() {
|
||||
|
@ -324,15 +333,21 @@
|
|||
}
|
||||
|
||||
handleBarcodeDetected(decodedText, decodedResult) {
|
||||
console.log(`QR Code terdeteksi: "${decodedText}"`);
|
||||
console.log(`Panjang kode: ${decodedText.length}`);
|
||||
|
||||
if (decodedText && decodedText.length >= 5) {
|
||||
console.log(`✅ Kode valid, memproses: ${decodedText}`);
|
||||
this.flashSuccess();
|
||||
this.playSuccessSound();
|
||||
this.vibrate();
|
||||
|
||||
this.detectedCode = decodedText;
|
||||
this.showResult(decodedText);
|
||||
this.stopScanner();
|
||||
this.playSuccessSound();
|
||||
this.stopScanner();
|
||||
|
||||
this.vibrate();
|
||||
this.processScanCode(decodedText);
|
||||
} else {
|
||||
console.log(`❌ Kode terlalu pendek: ${decodedText}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,11 +383,98 @@
|
|||
|
||||
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);
|
||||
|
||||
// Testing mode - uncomment kalau testing udah selesai
|
||||
this.mockResponse(code);
|
||||
return;
|
||||
|
||||
// ini bagian ajax yang asli
|
||||
/*
|
||||
$.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);
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
// Mock response untuk testing
|
||||
mockResponse(code) {
|
||||
console.log(`Testing scan untuk kode: ${code}`);
|
||||
|
||||
setTimeout(() => {
|
||||
const validCodes = [
|
||||
'SPJ001', 'SPJ002', 'SPJ003', 'SPJ004', 'SPJ005',
|
||||
'TEST123', 'TEST456', 'TEST789',
|
||||
'12345', '67890', '11111', '22222',
|
||||
'ABCDEF', 'GHIJKL', 'MNOPQR'
|
||||
];
|
||||
|
||||
const isValid = validCodes.some(validCode =>
|
||||
validCode.toLowerCase() === code.toLowerCase()
|
||||
);
|
||||
|
||||
if (isValid) {
|
||||
console.log(`✅ Kode ${code} VALID - menampilkan success`);
|
||||
|
||||
// Format tanggal Indonesia
|
||||
const now = new Date();
|
||||
const tanggal = now.toLocaleDateString('id-ID', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
const waktu = now.toLocaleTimeString('id-ID', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
this.showSuccessModal(code, tanggal, waktu);
|
||||
|
||||
setTimeout(() => {
|
||||
this.hideResult();
|
||||
this.manualInput.value = '';
|
||||
}, 3000);
|
||||
} else {
|
||||
console.log(`❌ Kode ${code} TIDAK VALID - menampilkan error`);
|
||||
this.showErrorModal(code);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async retryScan() {
|
||||
this.hideResult();
|
||||
this.hideError();
|
||||
|
@ -389,18 +491,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 +558,185 @@
|
|||
} 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"><svg class="w-10 h-10 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg></div>',
|
||||
'error': '<div class="w-20 h-20 mx-auto bg-red-100 rounded-full flex items-center justify-center shadow-lg"><svg class="w-10 h-10 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg></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 (!showCloseButton && type === 'loading') {
|
||||
setTimeout(() => {
|
||||
this.hideModal();
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
showSuccessModal(code, tanggal, waktu) {
|
||||
const successIcon = `
|
||||
<div class="w-28 h-28 mx-auto bg-gradient-to-r from-green-400 to-emerald-500 rounded-full flex items-center justify-center shadow-xl mb-6 animate-pulse">
|
||||
<svg class="w-16 h-16 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const successContent = `
|
||||
<div class="bg-gradient-to-br from-green-50 via-emerald-50 to-teal-50 rounded-2xl p-6 border border-green-200 shadow-inner">
|
||||
<div class="flex items-center justify-center mb-4">
|
||||
<div class="bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-xl px-4 py-2 shadow-lg">
|
||||
<span class="font-mono font-bold text-xl tracking-wider">${code}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-white rounded-xl p-4 shadow-sm border border-green-100">
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span class="text-gray-700 font-semibold text-lg">Status Aktif</span>
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="bg-white rounded-xl p-4 text-center shadow-sm border border-green-100">
|
||||
<div class="text-green-600 font-medium mb-2 text-sm">⏰ Waktu Scan</div>
|
||||
<div class="text-gray-800 font-bold text-lg">${waktu}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-xl p-4 text-center shadow-sm border border-green-100">
|
||||
<div class="text-green-600 font-medium mb-2 text-sm">📅 Tanggal</div>
|
||||
<div class="text-gray-800 font-bold text-sm leading-tight">${tanggal}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#modal-icon').html(successIcon);
|
||||
$('#modal-title').html('<span class="text-2xl font-bold bg-gradient-to-r from-green-600 to-emerald-600 bg-clip-text text-transparent">🎉 Scan Berhasil!</span>');
|
||||
$('#modal-message').html(successContent);
|
||||
$('#modal-close').hide(); // Sembunyikan tombol
|
||||
|
||||
$('#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);
|
||||
|
||||
setTimeout(() => {
|
||||
this.hideModal();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
showErrorModal(code) {
|
||||
const errorIcon = `
|
||||
<div class="w-28 h-28 mx-auto bg-gradient-to-r from-red-400 to-rose-500 rounded-full flex items-center justify-center shadow-xl mb-6 animate-pulse">
|
||||
<svg class="w-16 h-16 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const errorContent = `
|
||||
<div class="bg-gradient-to-br from-red-50 via-rose-50 to-pink-50 rounded-2xl p-6 border border-red-200 shadow-inner">
|
||||
<div class="flex items-center justify-center mb-4">
|
||||
<div class="bg-gradient-to-r from-red-500 to-rose-600 text-white rounded-xl px-4 py-2 shadow-lg">
|
||||
<span class="font-mono font-bold text-xl tracking-wider">${code}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-white rounded-xl p-4 shadow-sm border border-red-100">
|
||||
<div class="text-center">
|
||||
<div class="flex items-center justify-center mb-2">
|
||||
<svg class="w-6 h-6 text-red-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||
</svg>
|
||||
<span class="text-red-700 font-bold text-lg">SPJ Tidak Ditemukan</span>
|
||||
</div>
|
||||
<div class="text-red-600 text-sm opacity-90 leading-relaxed">
|
||||
Kode yang Anda masukkan tidak terdaftar dalam sistem.<br>
|
||||
Silakan periksa kembali atau hubungi administrator.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-50 rounded-xl p-4 border border-yellow-200">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-5 h-5 text-yellow-600 mr-2 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div class="text-yellow-800 text-sm">
|
||||
<div class="font-semibold mb-1">💡 Tips:</div>
|
||||
<div class="text-xs leading-relaxed">
|
||||
• Pastikan QR Code dalam kondisi baik<br>
|
||||
• Coba scan ulang dengan pencahayaan yang cukup<br>
|
||||
• Gunakan input manual jika diperlukan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#modal-icon').html(errorIcon);
|
||||
$('#modal-title').html('<span class="text-xl font-bold bg-gradient-to-r from-red-600 to-rose-600 bg-clip-text text-transparent">❌ Scan Gagal</span>');
|
||||
$('#modal-message').html(errorContent);
|
||||
$('#modal-close').hide(); // Sembunyikan tombol
|
||||
|
||||
$('#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);
|
||||
|
||||
setTimeout(() => {
|
||||
this.hideModal();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
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();
|
||||
|
|
|
@ -1,3 +1,94 @@
|
|||
/* Modal animations and enhancements */
|
||||
#scan-modal {
|
||||
backdrop-filter: blur(8px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#scan-modal .bg-white {
|
||||
transform-origin: center center;
|
||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||
}
|
||||
|
||||
/* Enhanced modal auto-hide animation */
|
||||
.modal-auto-hide {
|
||||
animation: modalAutoHide 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes modalAutoHide {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
80% {
|
||||
opacity: 1;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
/* Success modal specific animations */
|
||||
.success-gradient-bg {
|
||||
background: linear-gradient(135deg, #10b981, #059669, #047857);
|
||||
animation: gradientShift 2s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0%,
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Error modal specific animations */
|
||||
.error-gradient-bg {
|
||||
background: linear-gradient(135deg, #ef4444, #dc2626, #b91c1c);
|
||||
animation: gradientShift 2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Enhanced pulse animation for icons */
|
||||
.icon-pulse {
|
||||
animation: iconPulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes iconPulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* Card shadow effects */
|
||||
.card-shadow-success {
|
||||
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.card-shadow-error {
|
||||
box-shadow: 0 10px 25px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
/* Responsive modal improvements */
|
||||
@media (max-width: 640px) {
|
||||
#scan-modal .bg-white {
|
||||
margin: 16px;
|
||||
max-width: calc(100vw - 32px);
|
||||
}
|
||||
|
||||
#scan-modal .bg-white .space-y-4 > div {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.scanner-container {
|
||||
position: relative;
|
||||
background: #1a1a1a;
|
||||
|
@ -86,6 +177,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