pesapakawan/Views/Admin/Transport/SpjDriver/Login/Index.cshtml

880 lines
28 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

@{
Layout = "~/Views/Admin/Transport/SpjDriver/Shared/_Layout.cshtml";
ViewData["Title"] = "Login eSPJ";
}
<div class="bg-gradient-to-br from-indigo-50 via-white to-purple-50">
<div class="max-w-sm mx-auto bg-white min-h-screen shadow-xl relative overflow-hidden">
<!-- Splash Screens Container -->
<div id="splashContainer" class="splash-container">
<!-- Welcome Screen -->
<div class="slide">
<div class="slide-content flex flex-col items-center">
@* <div class="icon-circle">
<svg width="32" height="32" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 10V3L4 14h7v7l9-11h-7z" color="white"></path>
</svg>
</div> *@
<img class="w-20 h-20" src="@Url.Content("~/driver/logo.svg")" alt="">
<h2>Selamat Datang di eSPJ</h2>
<p>Aplikasi modern untuk pengelolaan Surat Perintah Jalan Driver yang efisien dan terintegrasi.</p>
</div>
</div>
<!-- Monitoring Screen -->
<div class="slide">
<div class="slide-content">
<div class="icon-circle">
<i class="w-10 h-10 text-white" data-lucide="home"></i>
</div>
<h2>Monitoring Real-Time</h2>
<p>Pantau status SPJ driver, kondisi kendaraan, dan muatan di setiap lokasi secara langsung.</p>
</div>
</div>
<!-- Integrasi Lengkap Screen -->
<div class="slide">
<div class="slide-content">
<div class="icon-circle">
<svg width="32" height="32" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 4v16m8-8H4" color="white"></path>
</svg>
</div>
<h2>Integrasi Lengkap</h2>
<p>Sistem terhubung antara admin, driver, dan manajemen untuk pengelolaan SPJ yang efisien.</p>
</div>
</div>
<!-- Login Screen -->
<div class="slide">
<div style="width: 100%; display: flex; align-items: center; justify-content: center; min-height: 100vh;">
<div class="login-form">
<div class="login-icon">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" color="white"></path>
</svg>
</div>
<div style="text-align: center; margin-bottom: 2rem;">
<h2 style="font-size: 1.5rem; font-weight: 700; color: #1f2937; margin-bottom: 0.5rem;">Masuk ke eSPJ</h2>
<p style="color: #6b7280; font-size: 0.9rem;">Gunakan Single Sign-On untuk akses yang aman</p>
</div>
<button class="sso-btn" onclick="showSSOModal()">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
</svg>
Masuk dengan SSO
</button>
</div>
</div>
</div>
</div>
<!-- Navigation Controls -->
<div id="navigationControls" class="navigation">
<!-- Dot Navigation -->
<div class="dots">
<div class="dot active" data-slide="0"></div>
<div class="dot" data-slide="1"></div>
<div class="dot" data-slide="2"></div>
<div class="dot" data-slide="3"></div>
</div>
<!-- Navigation Buttons -->
<div class="nav-buttons">
<button id="skipBtn" class="btn btn-skip">Lewati</button>
<button id="nextBtn" class="btn btn-primary">Selanjutnya</button>
</div>
</div>
<!-- SSO Webview Modal -->
<div id="ssoModal" class="webview-modal">
<div class="webview-container">
<div class="webview-header">
<div class="webview-title">Single Sign-On</div>
<button class="close-btn" onclick="closeSSOModal()">
<svg width="16" height="16" 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>
</button>
</div>
<div style="position: relative; height: calc(100% - 60px);">
<div id="loadingSpinner" class="loading-spinner"></div>
<iframe id="ssoIframe" class="webview-iframe" src="" style="display: none;"></iframe>
</div>
</div>
</div>
<!-- Update Available Notification -->
<div id="updateNotification" class="update-notification" style="display: none;">
<div class="update-content">
<div class="update-icon">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
</svg>
</div>
<div class="update-text">
<p>Update tersedia</p>
<small>Versi baru aplikasi sudah tersedia</small>
</div>
<button onclick="applyUpdate()" class="update-btn">Update</button>
</div>
</div>
</div>
</div>
@* Display any server-side validation errors *@
@if (ViewData.ModelState.ErrorCount > 0)
{
<div id="errorNotification" class="error-notification">
<div class="error-content">
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
{
<p>@error.ErrorMessage</p>
}
</div>
</div>
}
@section Styles {
<style>
.splash-container {
width: 400%;
display: flex;
transition: transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.slide {
width: 25%;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
position: relative;
}
.slide-content {
text-align: center;
animation: slideIn 0.8s ease-out;
}
@@keyframes slideIn {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.icon-circle {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #fb923c, #f59e42);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 2rem;
box-shadow: 0 10px 30px rgba(251, 146, 60, 0.3);
animation: pulse 2s infinite;
}
@@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.slide h2 {
font-size: 1.75rem;
font-weight: 700;
color: #1f2937;
margin-bottom: 1rem;
line-height: 1.2;
}
.slide p {
color: #6b7280;
font-size: 1rem;
line-height: 1.6;
}
.navigation {
position: absolute;
bottom: 2rem;
left: 0;
right: 0;
padding: 0 2rem;
}
.dots {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 2rem;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #e5e7eb;
transition: all 0.3s ease;
cursor: pointer;
}
.dot.active {
background: linear-gradient(135deg, #fb923c, #f59e42);
transform: scale(1.5);
}
.nav-buttons {
display: flex;
justify-content: space-between;
align-items: center;
}
.btn {
padding: 0.75rem 1.5rem;
border-radius: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
border: none;
font-size: 0.9rem;
}
.btn-skip {
background: transparent;
color: #6b7280;
}
.btn-skip:hover {
color: #374151;
}
.btn-primary {
background: linear-gradient(135deg, #fb923c, #f59e42);
color: white;
box-shadow: 0 4px 15px rgba(251, 146, 60, 0.3);
}
.btn-primary:hover {
box-shadow: 0 8px 25px rgba(251, 146, 60, 0.4);
transform: translateY(-2px);
}
.login-form {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20px);
border-radius: 24px;
padding: 2rem;
margin: 2rem;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
animation: fadeInUp 0.8s ease-out;
}
@@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.login-icon {
width: 60px;
height: 60px;
border-radius: 16px;
background: linear-gradient(135deg, #fb923c, #f59e42);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.5rem;
box-shadow: 0 8px 20px rgba(251, 146, 60, 0.3);
}
.sso-btn, .w-full {
width: 100%;
}
.sso-btn {
padding: 1rem;
background: linear-gradient(135deg, #fb923c, #f59e42);
color: white;
border: none;
border-radius: 16px;
font-weight: 600;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(251, 146, 60, 0.3);
}
.sso-btn:hover {
box-shadow: 0 8px 25px rgba(251, 146, 60, 0.4);
transform: translateY(-2px);
}
.webview-modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
display: none;
backdrop-filter: blur(4px);
}
.webview-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90vw;
max-width: 400px;
height: 80vh;
max-height: 600px;
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
animation: modalSlideIn 0.4s ease-out;
}
@@keyframes modalSlideIn {
from {
opacity: 0;
transform: translate(-50%, -60%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
.webview-header {
background: linear-gradient(135deg, #fb923c, #f59e42);
color: white;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.webview-title {
font-weight: 600;
font-size: 1rem;
}
.close-btn {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.webview-iframe {
width: 100%;
height: calc(100% - 60px);
border: none;
}
.loading-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
border: 3px solid #f3f4f6;
border-top: 3px solid #fb923c;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@@keyframes spin {
0% { transform: translate(-50%, -50%) rotate(0deg); }
100% { transform: translate(-50%, -50%) rotate(360deg); }
}
.update-notification {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
padding: 1rem;
z-index: 1001;
animation: slideDown 0.3s ease-out;
}
@@keyframes slideDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
.update-content {
display: flex;
align-items: center;
gap: 1rem;
}
.update-icon {
color: #fb923c;
}
.update-btn {
background: linear-gradient(135deg, #fb923c, #f59e42);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
.error-notification {
position: fixed;
top: 20px;
right: 20px;
background: #fee2e2;
border: 1px solid #fca5a5;
color: #dc2626;
padding: 1rem;
border-radius: 8px;
z-index: 1001;
animation: slideInRight 0.3s ease-out;
}
@@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* PWA Status Bar Safe Area */
@@supports (padding-top: env(safe-area-inset-top)) {
.max-w-sm {
padding-top: env(safe-area-inset-top);
}
}
/* Responsive Design */
@@media (max-width: 480px) {
.slide {
padding: 1.5rem;
}
.login-form {
margin: 1rem;
padding: 1.5rem;
}
.webview-container {
width: 95vw;
height: 85vh;
}
}
</style>
}
@section Scripts {
<script>
// PWA Service Worker Registration
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/driver/sw.js')
.then((registration) => {
console.log('SW registered: ', registration);
// Check for updates
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
showUpdateNotification();
}
});
});
})
.catch((registrationError) => {
console.log('SW registration failed: ', registrationError);
});
});
}
// Splash Screen Navigation
let currentSlide = 0;
const totalSlides = 4;
const container = document.getElementById('splashContainer');
const dots = document.querySelectorAll('.dot');
const nextBtn = document.getElementById('nextBtn');
const skipBtn = document.getElementById('skipBtn');
const navigationControls = document.getElementById('navigationControls');
function updateSlide(slideIndex) {
// Ensure slideIndex is within bounds
if (slideIndex < 0) slideIndex = 0;
if (slideIndex >= totalSlides) slideIndex = totalSlides - 1;
currentSlide = slideIndex;
container.style.transform = `translateX(-${slideIndex * 25}%)`;
// Update dots
dots.forEach((dot, index) => {
dot.classList.toggle('active', index === slideIndex);
});
// Update navigation visibility and button text
if (slideIndex === totalSlides - 1) {
navigationControls.style.display = 'none';
} else {
navigationControls.style.display = 'block';
nextBtn.textContent = slideIndex === totalSlides - 2 ? 'Masuk' : 'Selanjutnya';
}
}
function nextSlide() {
if (currentSlide < totalSlides - 1) {
currentSlide++;
updateSlide(currentSlide);
}
}
function skipToLogin() {
currentSlide = totalSlides - 1;
updateSlide(currentSlide);
}
// Event Listeners
nextBtn.addEventListener('click', nextSlide);
skipBtn.addEventListener('click', skipToLogin);
dots.forEach((dot, index) => {
dot.addEventListener('click', () => {
currentSlide = index;
updateSlide(currentSlide);
});
});
// Touch/Swipe Navigation
let startX = 0;
let endX = 0;
let isTouch = false;
container.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
isTouch = true;
}, { passive: true });
container.addEventListener('touchmove', (e) => {
if (!isTouch) return;
// Prevent default scrolling behavior during swipe
e.preventDefault();
}, { passive: false });
container.addEventListener('touchend', (e) => {
if (!isTouch) return;
endX = e.changedTouches[0].clientX;
handleSwipe();
isTouch = false;
}, { passive: true });
function handleSwipe() {
const swipeThreshold = 50;
const diff = startX - endX;
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0 && currentSlide < totalSlides - 1) {
// Swipe left - next slide
nextSlide();
} else if (diff < 0 && currentSlide > 0) {
// Swipe right - previous slide
currentSlide--;
updateSlide(currentSlide);
}
}
}
// SSO Modal Functions
function showSSOModal() {
const modal = document.getElementById('ssoModal');
const iframe = document.getElementById('ssoIframe');
const spinner = document.getElementById('loadingSpinner');
// Show modal
modal.style.display = 'block';
document.body.style.overflow = 'hidden';
// Show loading spinner
spinner.style.display = 'block';
iframe.style.display = 'none';
// Set SSO URL from server-side ViewBag
const ssoUrl = '@Html.Raw(ViewBag.SSOLoginUrl ?? "")';
if (ssoUrl) {
iframe.src = ssoUrl;
} else {
console.error('SSO URL not provided');
closeSSOModal();
alert('SSO tidak tersedia saat ini. Silakan gunakan login manual.');
return;
}
// Handle iframe load
iframe.onload = function() {
spinner.style.display = 'none';
iframe.style.display = 'block';
};
// Handle iframe error
iframe.onerror = function() {
spinner.style.display = 'none';
alert('Gagal memuat halaman SSO. Silakan coba lagi.');
closeSSOModal();
};
}
function closeSSOModal() {
const modal = document.getElementById('ssoModal');
const iframe = document.getElementById('ssoIframe');
modal.style.display = 'none';
document.body.style.overflow = 'auto';
iframe.src = '';
}
// Listen for SSO success message from iframe
window.addEventListener('message', function(event) {
// Verify origin for security (uncomment and set your SSO domain)
// const allowedOrigins = ['https://your-sso-domain.com'];
// if (!allowedOrigins.includes(event.origin)) return;
if (event.data === 'sso-success' || (event.data && event.data.type === 'sso-success')) {
closeSSOModal();
// Show success message
showNotification('Login berhasil! Mengarahkan...', 'success');
// Store login state
if (typeof(Storage) !== "undefined") {
sessionStorage.setItem('isLoggedIn', 'true');
sessionStorage.setItem('loginTime', new Date().toISOString());
}
// Redirect after short delay
setTimeout(() => {
window.location.href = '@Url.Action("Index", "Home")' || '/';
}, 1500);
} else if (event.data === 'sso-error' || (event.data && event.data.type === 'sso-error')) {
closeSSOModal();
showNotification('Login gagal. Silakan coba lagi.', 'error');
}
});
// Handle back button on Android PWA
window.addEventListener('popstate', function(event) {
const modal = document.getElementById('ssoModal');
if (modal.style.display === 'block') {
closeSSOModal();
event.preventDefault();
return false;
}
});
// PWA Update Functions
function showUpdateNotification() {
const notification = document.getElementById('updateNotification');
if (notification) {
notification.style.display = 'block';
// Auto-hide after 10 seconds
setTimeout(() => {
notification.style.display = 'none';
}, 10000);
}
}
function applyUpdate() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistration().then((registration) => {
if (registration && registration.waiting) {
registration.waiting.postMessage({ type: 'SKIP_WAITING' });
window.location.reload();
}
});
}
}
// Notification System
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<div class="notification-content">
<span>${message}</span>
<button onclick="this.parentElement.parentElement.remove()">×</button>
</div>
`;
// Add notification styles
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: ${type === 'success' ? '#dcfce7' : type === 'error' ? '#fee2e2' : '#f3f4f6'};
color: ${type === 'success' ? '#166534' : type === 'error' ? '#dc2626' : '#374151'};
border: 1px solid ${type === 'success' ? '#bbf7d0' : type === 'error' ? '#fca5a5' : '#d1d5db'};
padding: 1rem;
border-radius: 8px;
z-index: 1002;
animation: slideInRight 0.3s ease-out;
max-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
`;
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 5000);
}
// Handle connection status
function updateConnectionStatus() {
const isOnline = navigator.onLine;
if (!isOnline) {
showNotification('Koneksi internet terputus. Beberapa fitur mungkin tidak tersedia.', 'error');
}
}
window.addEventListener('online', () => {
showNotification('Koneksi internet kembali terhubung.', 'success');
});
window.addEventListener('offline', updateConnectionStatus);
// Initialize everything when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM Content Loaded');
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Auto-hide error notifications after page load
const errorNotification = document.getElementById('errorNotification');
if (errorNotification) {
setTimeout(() => {
errorNotification.style.display = 'none';
}, 5000);
}
// Initialize splash screen - ALWAYS start from first slide
currentSlide = 0;
updateSlide(0);
console.log('Splash screen initialized at slide:', currentSlide);
// Optional: Check if user is a returning visitor and show a different experience
// But don't automatically skip the splash screen
const isReturningUser = sessionStorage.getItem('hasSeenSplash') === 'true';
if (isReturningUser) {
// You can add a "Skip Intro" button or similar UX improvement
console.log('Returning user detected');
// But still show the splash screen by default
} else {
sessionStorage.setItem('hasSeenSplash', 'true');
}
});
// PWA Install Prompt
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later
deferredPrompt = e;
// Show install button/notification
showInstallPrompt();
});
function showInstallPrompt() {
const installNotification = document.createElement('div');
installNotification.innerHTML = `
<div style="position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: white; padding: 1rem; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); z-index: 1001; text-align: center;">
<p style="margin-bottom: 1rem; color: #374151;">Install eSPJ untuk pengalaman yang lebih baik</p>
<button onclick="installPWA()" style="background: linear-gradient(135deg, #fb923c, #f59e42); color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 8px; margin-right: 0.5rem; cursor: pointer;">Install</button>
<button onclick="this.parentElement.parentElement.remove()" style="background: #6b7280; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 8px; cursor: pointer;">Nanti</button>
</div>
`;
document.body.appendChild(installNotification);
}
function installPWA() {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((result) => {
if (result.outcome === 'accepted') {
console.log('User accepted the install prompt');
showNotification('Aplikasi berhasil diinstall!', 'success');
}
deferredPrompt = null;
});
}
}
</script>
}