1088 lines
		
	
	
		
			49 KiB
		
	
	
	
		
			Plaintext
		
	
	
			
		
		
	
	
			1088 lines
		
	
	
		
			49 KiB
		
	
	
	
		
			Plaintext
		
	
	
@{
 | 
						|
    Layout = "~/Views/Admin/Transport/SpjAdmin/Shared/_Layout.cshtml";
 | 
						|
    ViewData["Title"] = "Scan SPJ";
 | 
						|
}
 | 
						|
 | 
						|
@section Styles {
 | 
						|
    <link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" />
 | 
						|
    <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
 | 
						|
    
 | 
						|
    <style>
 | 
						|
        .select2-container {
 | 
						|
            width: 100% !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-container--default .select2-selection--single {
 | 
						|
            height: 42px !important;
 | 
						|
            border: 1px solid #d1d5db !important;
 | 
						|
            border-radius: 0.5rem !important;
 | 
						|
            padding: 0 12px !important;
 | 
						|
            display: flex !important;
 | 
						|
            align-items: center !important;
 | 
						|
            font-size: 14px !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-container--default .select2-selection--single:focus {
 | 
						|
            border-color: #f97316 !important;
 | 
						|
            box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2) !important;
 | 
						|
            outline: none !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-container--default .select2-selection--single .select2-selection__rendered {
 | 
						|
            color: #374151 !important;
 | 
						|
            line-height: normal !important;
 | 
						|
            padding: 0 !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-container--default .select2-selection--single .select2-selection__placeholder {
 | 
						|
            color: #9ca3af !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-container--default .select2-selection--single .select2-selection__arrow {
 | 
						|
            height: 40px !important;
 | 
						|
            right: 8px !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-dropdown {
 | 
						|
            border: 1px solid #d1d5db !important;
 | 
						|
            border-radius: 0.5rem !important;
 | 
						|
            box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-container--default .select2-results__option {
 | 
						|
            padding: 12px 16px !important;
 | 
						|
            font-size: 14px !important;
 | 
						|
            border-bottom: 1px solid #f3f4f6 !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-container--default .select2-results__option:last-child {
 | 
						|
            border-bottom: none !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-container--default .select2-results__option--highlighted[aria-selected] {
 | 
						|
            background-color: #f97316 !important;
 | 
						|
            color: white !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-container--default .select2-results__option[aria-selected=true] {
 | 
						|
            background-color: #fed7aa !important;
 | 
						|
            color: #ea580c !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-search--dropdown .select2-search__field {
 | 
						|
            border: 1px solid #d1d5db !important;
 | 
						|
            border-radius: 0.375rem !important;
 | 
						|
            padding: 8px 12px !important;
 | 
						|
            font-size: 14px !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-search--dropdown .select2-search__field:focus {
 | 
						|
            border-color: #f97316 !important;
 | 
						|
            box-shadow: 0 0 0 2px rgba(249, 115, 22, 0.2) !important;
 | 
						|
            outline: none !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .select2-results__message {
 | 
						|
            padding: 12px 16px !important;
 | 
						|
            color: #6b7280 !important;
 | 
						|
            font-size: 14px !important;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .spj-option {
 | 
						|
            display: flex;
 | 
						|
            flex-direction: column;
 | 
						|
            gap: 4px;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .spj-code {
 | 
						|
            font-weight: 600;
 | 
						|
            color: #1f2937;
 | 
						|
            font-family: 'Courier New', monospace;
 | 
						|
        }
 | 
						|
        
 | 
						|
        .spj-details {
 | 
						|
            font-size: 12px;
 | 
						|
            color: #6b7280;
 | 
						|
            display: flex;
 | 
						|
            justify-content: space-between;
 | 
						|
            align-items: center;
 | 
						|
        }
 | 
						|
    </style>
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
<div class="w-full lg:max-w-sm mx-auto bg-white min-h-screen">
 | 
						|
    <div class="bg-orange-500 text-white px-3 py-4 rounded-b-2xl relative pb-12">
 | 
						|
        <div class="flex items-center justify-between">
 | 
						|
            <a href="@Url.Action("Index", "Home")" class="p-1 hover:bg-white/10 rounded-full transition-colors">
 | 
						|
                <i class="w-5 h-5" data-lucide="chevron-left"></i>
 | 
						|
            </a>
 | 
						|
            <h1 class="text-lg font-bold">Scan SPJ</h1>
 | 
						|
            <div class="w-8"></div>
 | 
						|
        </div>
 | 
						|
    </div>
 | 
						|
 | 
						|
    <div class="p-4">
 | 
						|
        @if (TempData["Success"] != null)
 | 
						|
        {
 | 
						|
            <div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
 | 
						|
                <div class="flex items-center">
 | 
						|
                    <i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
 | 
						|
                    <span class="text-green-800">@TempData["Success"]</span>
 | 
						|
                </div>
 | 
						|
            </div>
 | 
						|
        }
 | 
						|
 | 
						|
        @if (TempData["Error"] != null)
 | 
						|
        {
 | 
						|
            <div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
 | 
						|
                <div class="flex items-center">
 | 
						|
                    <i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
 | 
						|
                    <span class="text-red-800">@TempData["Error"]</span>
 | 
						|
                </div>
 | 
						|
            </div>
 | 
						|
        }
 | 
						|
 | 
						|
        <div class="scanner-container mb-4" style="height: 300px;">
 | 
						|
            <div id="scanner-container" class="w-full h-full relative bg-gray-900 rounded-lg overflow-hidden">
 | 
						|
                
 | 
						|
                <div id="loading-scanner" class="absolute inset-0 bg-gray-900 flex items-center justify-center z-10">
 | 
						|
                    <div class="text-center text-white">
 | 
						|
                        <div class="loading-spinner mx-auto mb-2"></div>
 | 
						|
                        <p class="text-sm">Memuat scanner...</p>
 | 
						|
                    </div>
 | 
						|
                </div>
 | 
						|
 | 
						|
                <!-- Status indicator ketika scanning aktif -->
 | 
						|
                <div id="scanning-indicator" class="absolute top-2 left-2 bg-green-500 text-white px-3 py-1 rounded-full text-xs font-medium z-20 hidden animate-pulse">
 | 
						|
                    <div class="flex items-center">
 | 
						|
                        <div class="w-2 h-2 bg-white rounded-full mr-2 animate-ping"></div>
 | 
						|
                        Scanner Aktif
 | 
						|
                    </div>
 | 
						|
                </div>
 | 
						|
            </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <div class="space-y-3 mb-4">
 | 
						|
            <button id="start-scanner" class="w-full bg-orange-500 hover:bg-orange-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner">
 | 
						|
                <i class="w-5 h-5 inline mr-2" data-lucide="camera"></i>
 | 
						|
                Mulai Scan
 | 
						|
            </button>
 | 
						|
            
 | 
						|
            <button id="stop-scanner" class="w-full bg-red-500 hover:bg-red-600 text-white font-medium py-3 px-4 rounded-lg transition-colors btn-scanner hidden">
 | 
						|
                <i class="w-5 h-5 inline mr-2" data-lucide="camera-off"></i>
 | 
						|
                Hentikan Scan
 | 
						|
            </button>
 | 
						|
 | 
						|
            <div id="permission-info" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-3">
 | 
						|
                <div class="flex items-start">
 | 
						|
                    <i class="w-5 h-5 text-blue-600 mr-2 mt-0.5" data-lucide="info"></i>
 | 
						|
                    <div class="text-blue-800 text-sm">
 | 
						|
                        <p class="font-medium mb-1">🎥 Meminta Akses Kamera...</p>
 | 
						|
                        <p class="mb-2">Browser akan meminta izin akses kamera. Pastikan untuk:</p>
 | 
						|
                        <ul class="text-xs space-y-1 list-disc list-inside">
 | 
						|
                            <li>Klik tombol <strong>"Allow"</strong> atau <strong>"Izinkan"</strong></li>
 | 
						|
                            <li>Jika popup tidak muncul, cek address bar browser</li>
 | 
						|
                            <li>Pastikan kamera tidak sedang digunakan aplikasi lain</li>
 | 
						|
                        </ul>
 | 
						|
                    </div>
 | 
						|
                </div>
 | 
						|
            </div>
 | 
						|
 | 
						|
            <div id="permission-denied" class="hidden bg-red-50 border border-red-200 rounded-lg p-3">
 | 
						|
                <div class="flex items-start">
 | 
						|
                    <i class="w-5 h-5 text-red-600 mr-2 mt-0.5" data-lucide="alert-triangle"></i>
 | 
						|
                    <div class="text-red-800 text-sm">
 | 
						|
                        <p class="font-medium mb-1">Akses Kamera Ditolak</p>
 | 
						|
                        <p class="mb-2">Untuk menggunakan scanner, aktifkan akses kamera:</p>
 | 
						|
                        <ol class="list-decimal list-inside space-y-1 text-xs">
 | 
						|
                            <li>Klik ikon kunci/kamera di address bar browser</li>
 | 
						|
                            <li>Pilih "Allow" atau "Izinkan" untuk kamera</li>
 | 
						|
                            <li>Refresh halaman dan coba lagi</li>
 | 
						|
                        </ol>
 | 
						|
                    </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="#" novalidate>
 | 
						|
                @Html.AntiForgeryToken()
 | 
						|
                <div class="space-y-3">
 | 
						|
                    <div>
 | 
						|
                        <select id="manual-barcode-select" 
 | 
						|
                                name="barcode" 
 | 
						|
                                class="w-full" 
 | 
						|
                                style="width: 100%;">
 | 
						|
                            <option value="">Ketik untuk mencari SPJ...</option>
 | 
						|
                        </select>
 | 
						|
                    </div>
 | 
						|
                    
 | 
						|
                    <button type="submit" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-3 px-4 rounded-lg transition-colors">
 | 
						|
                        <i class="w-5 h-5 inline mr-2" data-lucide="search"></i>
 | 
						|
                        Proses SPJ
 | 
						|
                    </button>
 | 
						|
                </div>
 | 
						|
            </form>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <div id="scan-result" class="hidden mt-4 p-4 bg-green-50 border border-green-200 rounded-lg scan-result-card">
 | 
						|
            <div class="flex items-center mb-2">
 | 
						|
                <i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
 | 
						|
                <span class="text-green-800 font-medium">QR Code terdeteksi!</span>
 | 
						|
            </div>
 | 
						|
            <p class="text-green-700 mb-3">Kode: <span id="detected-code" class="font-mono font-bold"></span></p>
 | 
						|
            <div class="flex gap-2">
 | 
						|
                <button id="confirm-scan" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors btn-scanner">
 | 
						|
                    Konfirmasi
 | 
						|
                </button>
 | 
						|
                <button id="retry-scan" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors btn-scanner">
 | 
						|
                    Scan Ulang
 | 
						|
                </button>
 | 
						|
            </div>
 | 
						|
        </div>
 | 
						|
 | 
						|
        <div id="error-message" class="hidden mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
 | 
						|
            <div class="flex items-center">
 | 
						|
                <i class="w-5 h-5 text-red-600 mr-2" data-lucide="alert-circle"></i>
 | 
						|
                <span class="text-red-800" id="error-text"></span>
 | 
						|
            </div>
 | 
						|
        </div>
 | 
						|
    </div>
 | 
						|
    <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 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://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
 | 
						|
    <script src="https://unpkg.com/html5-qrcode@2.3.8/html5-qrcode.min.js" type="text/javascript"></script>
 | 
						|
    
 | 
						|
    <script>
 | 
						|
        if (typeof Html5Qrcode === 'undefined') {
 | 
						|
            const script = document.createElement('script');
 | 
						|
            script.src = 'https://cdn.jsdelivr.net/npm/html5-qrcode@2.3.8/html5-qrcode.min.js';
 | 
						|
            script.onerror = () => alert('Scanner library failed to load');
 | 
						|
            document.head.appendChild(script);
 | 
						|
        }
 | 
						|
    </script>
 | 
						|
    
 | 
						|
    <script>
 | 
						|
        function initializeSelect2() {
 | 
						|
            $('#manual-barcode-select').select2({
 | 
						|
                placeholder: 'Ketik untuk mencari SPJ...',
 | 
						|
                allowClear: true,
 | 
						|
                minimumInputLength: 2,
 | 
						|
                
 | 
						|
                ajax: {
 | 
						|
                    url: '@Url.Action("SearchSpj", "SpjAdmin")',
 | 
						|
                    dataType: 'json',
 | 
						|
                    delay: 300,
 | 
						|
                    data: function (params) {
 | 
						|
                        return {
 | 
						|
                            q: params.term,
 | 
						|
                            page: params.page || 1,
 | 
						|
                            pageSize: 20
 | 
						|
                        };
 | 
						|
                    },
 | 
						|
                    processResults: function (data, params) {
 | 
						|
                        params.page = params.page || 1;
 | 
						|
                        
 | 
						|
                        return {
 | 
						|
                            results: data.items.map(function(item) {
 | 
						|
                                return {
 | 
						|
                                    id: item.id,
 | 
						|
                                    text: item.spjCode,
 | 
						|
                                    driverName: item.driverName,
 | 
						|
                                    platNomor: item.platNomor,
 | 
						|
                                    nomorPintu: item.nomorPintu
 | 
						|
                                };
 | 
						|
                            }),
 | 
						|
                            pagination: {
 | 
						|
                                more: data.hasMore
 | 
						|
                            }
 | 
						|
                        };
 | 
						|
                    },
 | 
						|
                    cache: true
 | 
						|
                },
 | 
						|
                
 | 
						|
                templateResult: formatSpjOption,
 | 
						|
                templateSelection: formatSpjSelection,
 | 
						|
                escapeMarkup: function(markup) {
 | 
						|
                    return markup;
 | 
						|
                }
 | 
						|
            });
 | 
						|
 | 
						|
            
 | 
						|
            $('#manual-barcode-select').on('select2:open', function (e) {
 | 
						|
                setTimeout(function() {
 | 
						|
                    scrollToSelect2();
 | 
						|
                }, 100);
 | 
						|
            });
 | 
						|
 | 
						|
            
 | 
						|
            $(document).on('focus', '.select2-search__field', function() {
 | 
						|
                setTimeout(function() {
 | 
						|
                    scrollToSelect2();
 | 
						|
                }, 300);
 | 
						|
            });
 | 
						|
        }
 | 
						|
 | 
						|
        function scrollToSelect2() {
 | 
						|
        const selectElement = document.getElementById('manual-barcode-select');
 | 
						|
        const select2Container = selectElement?.nextElementSibling;
 | 
						|
            
 | 
						|
            if (select2Container) {
 | 
						|
                select2Container.scrollIntoView({
 | 
						|
                    behavior: 'smooth',
 | 
						|
                    block: 'center', 
 | 
						|
                    inline: 'nearest'
 | 
						|
                });
 | 
						|
                
 | 
						|
                if (window.innerWidth <= 768) {
 | 
						|
                    const rect = select2Container.getBoundingClientRect();
 | 
						|
                    const scrollTop = window.pageYOffset + rect.top - (window.innerHeight / 3);
 | 
						|
                    
 | 
						|
                    window.scrollTo({
 | 
						|
                        top: scrollTop,
 | 
						|
                        behavior: 'smooth'
 | 
						|
                    });
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        function formatSpjOption(spj) {
 | 
						|
            if (spj.loading) {
 | 
						|
                return spj.text;
 | 
						|
            }
 | 
						|
 | 
						|
            if (!spj.driverName) {
 | 
						|
                return spj.text;
 | 
						|
            }
 | 
						|
 | 
						|
            return $(
 | 
						|
                '<div class="spj-option">' +
 | 
						|
                    '<div class="spj-code">' + spj.platNomor + '|' + spj.nomorPintu + '</div>' +
 | 
						|
                    @* '<div class="spj-details">' +
 | 
						|
                        '<span>' + spj.driverName + '</span>' +
 | 
						|
                        '<span class="bg-orange-300 rounded-full px-2 py-1 text-xs text-gray-500">' + spj.nomorPintu + '</span>' +
 | 
						|
                    '</div>' + *@
 | 
						|
                    '<div style="font-size: 11px; color: #9ca3af; font-family: monospace;">' + spj.text + '</div>' +
 | 
						|
                '</div>'
 | 
						|
            );
 | 
						|
        }
 | 
						|
 | 
						|
        function formatSpjSelection(spj) {
 | 
						|
            return spj.text || spj.id;
 | 
						|
        }
 | 
						|
 | 
						|
        function updateManualFormHandler() {
 | 
						|
            const manualInput = document.getElementById('manual-barcode-select');
 | 
						|
            const scanner = window.barcodeScanner; 
 | 
						|
            
 | 
						|
            if (manualInput && scanner) {
 | 
						|
                scanner.manualInput = manualInput;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
       class BarcodeScanner {
 | 
						|
            constructor() {
 | 
						|
                this.isScanning = false;
 | 
						|
                this.detectedCode = null;
 | 
						|
                this.html5QrCode = null;
 | 
						|
 | 
						|
                this.isProcessing = false;
 | 
						|
                this.lastCode = null;
 | 
						|
                this.lastScanTime = 0;
 | 
						|
                this.resumeTimer = null;
 | 
						|
                
 | 
						|
                this.initializeElements();
 | 
						|
                this.bindEvents();
 | 
						|
                this.checkBrowserSupport();
 | 
						|
            }
 | 
						|
 | 
						|
            initializeElements() {
 | 
						|
                this.startBtn = document.getElementById('start-scanner');
 | 
						|
                this.stopBtn = document.getElementById('stop-scanner');
 | 
						|
                this.loadingDiv = document.getElementById('loading-scanner');
 | 
						|
                this.scanResult = document.getElementById('scan-result');
 | 
						|
                this.errorMessage = document.getElementById('error-message');
 | 
						|
                this.detectedCodeSpan = document.getElementById('detected-code');
 | 
						|
                this.confirmBtn = document.getElementById('confirm-scan');
 | 
						|
                this.retryBtn = document.getElementById('retry-scan');
 | 
						|
                this.manualForm = document.getElementById('manual-form');
 | 
						|
                this.manualInput = document.getElementById('manual-barcode-select');
 | 
						|
                this.permissionInfo = document.getElementById('permission-info');
 | 
						|
                this.permissionDenied = document.getElementById('permission-denied');
 | 
						|
                this.scanningIndicator = document.getElementById('scanning-indicator');
 | 
						|
            }
 | 
						|
 | 
						|
            bindEvents() {
 | 
						|
                this.startBtn.addEventListener('click', () => this.startScanner());
 | 
						|
                this.stopBtn.addEventListener('click', () => this.stopScanner());
 | 
						|
                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() {
 | 
						|
                if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
 | 
						|
                    this.startBtn.disabled = true;
 | 
						|
                    this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Browser Tidak Didukung';
 | 
						|
                    this.startBtn.classList.remove('bg-orange-500', 'hover:bg-orange-600');
 | 
						|
                    this.startBtn.classList.add('bg-gray-400', 'cursor-not-allowed');
 | 
						|
                    this.showError('Browser Anda tidak mendukung akses kamera. Gunakan browser modern seperti Chrome, Firefox, atau Safari.');
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
 | 
						|
                if (typeof Html5Qrcode === 'undefined') {
 | 
						|
                    this.startBtn.disabled = true;
 | 
						|
                    this.startBtn.innerHTML = '<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>Library Tidak Dimuat';
 | 
						|
                    this.startBtn.classList.remove('bg-orange-500', 'hover:bg-orange-600');
 | 
						|
                    this.startBtn.classList.add('bg-gray-400', 'cursor-not-allowed');
 | 
						|
                    this.showError('Library scanner tidak dapat dimuat. Periksa koneksi internet dan refresh halaman.');
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
 | 
						|
                if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
 | 
						|
                    this.showError('Scanner barcode memerlukan koneksi HTTPS yang aman. Hubungi administrator sistem.');
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            async startScanner() {
 | 
						|
                try {
 | 
						|
                    this.showLoading();
 | 
						|
                    this.hideError();
 | 
						|
                    this.hideResult();
 | 
						|
                    this.hidePermissionMessages();
 | 
						|
 | 
						|
                    await this.initializeHtml5QrCode();
 | 
						|
                    
 | 
						|
                    this.isScanning = true;
 | 
						|
                    this.startBtn.classList.add('hidden');
 | 
						|
                    this.stopBtn.classList.remove('hidden');
 | 
						|
                    this.scanningIndicator.classList.remove('hidden');
 | 
						|
                    this.hideLoading();
 | 
						|
                    
 | 
						|
                } catch (error) {
 | 
						|
                    this.handleScannerError(error);
 | 
						|
                    this.hideLoading();
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            async initializeHtml5QrCode() {
 | 
						|
                try {
 | 
						|
                    this.permissionInfo.classList.remove('hidden');
 | 
						|
                    
 | 
						|
                    await new Promise(resolve => setTimeout(resolve, 1000));
 | 
						|
                    
 | 
						|
                    this.html5QrCode = new Html5Qrcode("scanner-container");
 | 
						|
                    
 | 
						|
                    const cameras = await Html5Qrcode.getCameras();
 | 
						|
                    
 | 
						|
                    if (cameras && cameras.length > 0) {
 | 
						|
                        let cameraId = cameras[0].id;
 | 
						|
                        
 | 
						|
                        const backCamera = cameras.find(camera => 
 | 
						|
                            camera.label.toLowerCase().includes('back') || 
 | 
						|
                            camera.label.toLowerCase().includes('rear') ||
 | 
						|
                            camera.label.toLowerCase().includes('environment')
 | 
						|
                        );
 | 
						|
                        
 | 
						|
                        if (backCamera) {
 | 
						|
                            cameraId = backCamera.id;
 | 
						|
                        }
 | 
						|
                        
 | 
						|
                        await this.html5QrCode.start(
 | 
						|
                            cameraId,
 | 
						|
                            {
 | 
						|
                                fps: 10,
 | 
						|
                                qrbox: function(viewfinderWidth, viewfinderHeight) {
 | 
						|
                                    let minEdgePercentage = 0.7;
 | 
						|
                                    let minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight);
 | 
						|
                                    let qrboxSize = Math.floor(minEdgeSize * minEdgePercentage);
 | 
						|
                                    return {
 | 
						|
                                        width: qrboxSize,
 | 
						|
                                        height: qrboxSize
 | 
						|
                                    };
 | 
						|
                                },
 | 
						|
                                aspectRatio: 1.0
 | 
						|
                            },
 | 
						|
                            (decodedText, decodedResult) => {
 | 
						|
                                this.handleBarcodeDetected(decodedText, decodedResult);
 | 
						|
                            },
 | 
						|
                            (errorMessage) => {
 | 
						|
                            }
 | 
						|
                        );
 | 
						|
                        
 | 
						|
                        this.hidePermissionMessages();
 | 
						|
                        
 | 
						|
                    } else {
 | 
						|
                        throw new Error('No cameras found on this device');
 | 
						|
                    }
 | 
						|
                    
 | 
						|
                } catch (error) {
 | 
						|
                    this.hidePermissionMessages();
 | 
						|
                    
 | 
						|
                    if (error.message.includes('Permission denied') || 
 | 
						|
                        error.message.includes('NotAllowedError') ||
 | 
						|
                        error.message.includes('permission') ||
 | 
						|
                        error.name === 'NotAllowedError') {
 | 
						|
                        this.permissionDenied.classList.remove('hidden');
 | 
						|
                        throw new Error('Camera permission denied');
 | 
						|
                    } else if (error.message.includes('No cameras found')) {
 | 
						|
                        throw new Error('No camera found on this device');
 | 
						|
                    } else {
 | 
						|
                        throw new Error('Unable to access camera: ' + error.message);
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            handleScannerError(error) {
 | 
						|
                if (error.message.includes('permission denied') || error.message.includes('Camera permission denied')) {
 | 
						|
                    this.permissionDenied.classList.remove('hidden');
 | 
						|
                } else if (error.message.includes('No camera found')) {
 | 
						|
                    this.showError('Kamera tidak ditemukan pada perangkat ini.');
 | 
						|
                } else if (error.message.includes('NotReadableError')) {
 | 
						|
                    this.showError('Kamera sedang digunakan aplikasi lain. Tutup aplikasi lain dan coba lagi.');
 | 
						|
                } else {
 | 
						|
                    this.showError('Gagal memulai scanner. Pastikan kamera dapat diakses dan coba lagi.');
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            handleBarcodeDetected(decodedText, decodedResult) {
 | 
						|
                console.log(`QR Code terdeteksi: "${decodedText}"`);
 | 
						|
                console.log(`Panjang kode: ${decodedText.length}`);
 | 
						|
                
 | 
						|
                const code = typeof decodedText === 'string' ? decodedText.trim() : '';
 | 
						|
                if (!code || code.length < 5) {
 | 
						|
                    console.log(`❌ Kode tidak valid atau terlalu pendek: ${decodedText}`);
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
                if (this.isProcessing) {
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
                const now = Date.now();
 | 
						|
                if (code === this.lastCode && (now - this.lastScanTime) < 3000) {
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
                this.lastCode = code;
 | 
						|
                this.lastScanTime = now;
 | 
						|
 | 
						|
                console.log(`✅ Kode valid, memproses: ${code}`);
 | 
						|
                this.flashSuccess();
 | 
						|
                this.playSuccessSound();
 | 
						|
                this.vibrate();
 | 
						|
 | 
						|
                this.detectedCode = code;
 | 
						|
                this.isProcessing = true;
 | 
						|
                
 | 
						|
                // Simpan SPJ code asli untuk ditampilkan di modal
 | 
						|
                this.originalSpjCode = code;
 | 
						|
 | 
						|
                try {
 | 
						|
                    if (this.html5QrCode && typeof this.html5QrCode.pause === 'function') {
 | 
						|
                        this.html5QrCode.pause(true);
 | 
						|
                    }
 | 
						|
                } catch (e) {}
 | 
						|
 | 
						|
                this.processScanCode(code);
 | 
						|
            }
 | 
						|
 | 
						|
            async stopScanner() {
 | 
						|
                if (this.isScanning && this.html5QrCode) {
 | 
						|
                    try {
 | 
						|
                        await this.html5QrCode.stop();
 | 
						|
                    } catch (error) {
 | 
						|
                    }
 | 
						|
                    this.isScanning = false;
 | 
						|
                }
 | 
						|
                this.isProcessing = false;
 | 
						|
                this.lastCode = null;
 | 
						|
                this.lastScanTime = 0;
 | 
						|
                if (this.resumeTimer) {
 | 
						|
                    clearTimeout(this.resumeTimer);
 | 
						|
                    this.resumeTimer = null;
 | 
						|
                }
 | 
						|
                
 | 
						|
                this.startBtn.classList.remove('hidden');
 | 
						|
                this.stopBtn.classList.add('hidden');
 | 
						|
                this.scanningIndicator.classList.add('hidden');
 | 
						|
            }
 | 
						|
 | 
						|
            flashSuccess() {
 | 
						|
                const flash = document.createElement('div');
 | 
						|
                flash.className = 'absolute inset-0 bg-green-500 opacity-50 rounded-lg';
 | 
						|
                flash.style.zIndex = '20';
 | 
						|
                document.getElementById('scanner-container').appendChild(flash);
 | 
						|
                
 | 
						|
                setTimeout(() => {
 | 
						|
                    flash.remove();
 | 
						|
                }, 200);
 | 
						|
            }
 | 
						|
 | 
						|
            vibrate() {
 | 
						|
                if ('vibrate' in navigator) {
 | 
						|
                    navigator.vibrate([200]); 
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            confirmScan() {
 | 
						|
                if (this.detectedCode) {
 | 
						|
                    // Simpan SPJ code asli untuk ditampilkan di modal
 | 
						|
                    this.originalSpjCode = this.detectedCode;
 | 
						|
                    this.processScanCode(this.detectedCode);
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            isGuid(value) {
 | 
						|
                const re = /^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$/;
 | 
						|
                return re.test((value || '').trim());
 | 
						|
            }
 | 
						|
 | 
						|
            isSpjCode(value) {
 | 
						|
                const re = /^SPJ\/\d{2}-\d{4}\/[A-Z]+\/\d{6}$/i;
 | 
						|
                return re.test((value || '').trim());
 | 
						|
            }
 | 
						|
 | 
						|
            processUrlFor(id) {
 | 
						|
                const basePath = '@Url.Content("~/transport/spj-adminscan/process/")';
 | 
						|
                return basePath + encodeURIComponent(id);
 | 
						|
            }
 | 
						|
 | 
						|
            postGuid(id) {
 | 
						|
                const url = this.processUrlFor(id);
 | 
						|
                return $.ajax({
 | 
						|
                    url: url,
 | 
						|
                    type: 'POST',
 | 
						|
                    headers: { 'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val() }
 | 
						|
                });
 | 
						|
            }
 | 
						|
 | 
						|
            resolveSpj(code) {
 | 
						|
                const url = '@Url.Content("~/transport/spj-adminscan/resolve")';
 | 
						|
                return $.ajax({
 | 
						|
                    url: url,
 | 
						|
                    type: 'POST',
 | 
						|
                    headers: { 'RequestVerificationToken': $('input[name="__RequestVerificationToken"]').val() },
 | 
						|
                    data: { code }
 | 
						|
                });
 | 
						|
            }
 | 
						|
 | 
						|
            async processScanCode(code) {
 | 
						|
                this.showModal('loading', 'Memproses...', 'Sedang memverifikasi kode SPJ...', false);
 | 
						|
 | 
						|
                try {
 | 
						|
                    let guid = null;
 | 
						|
                    const trimmed = (code || '').trim();
 | 
						|
                    
 | 
						|
                    if (!this.originalSpjCode) {
 | 
						|
                        this.originalSpjCode = trimmed;
 | 
						|
                    }
 | 
						|
 | 
						|
                    if (this.isGuid(trimmed)) {
 | 
						|
                        guid = trimmed;
 | 
						|
                    } else if (this.isSpjCode(trimmed)) {
 | 
						|
                        const rs = await this.resolveSpj(trimmed);
 | 
						|
                        if (!rs || rs.success !== true || !rs.id) {
 | 
						|
                            throw new Error((rs && rs.message) ? rs.message : 'Gagal resolve SPJ ke GUID.');
 | 
						|
                        }
 | 
						|
                        guid = rs.id;
 | 
						|
                    } else {
 | 
						|
                        throw new Error('Format kode tidak dikenali. Masukkan GUID atau nomor SPJ yang valid.');
 | 
						|
                    }
 | 
						|
 | 
						|
                    const resp = await this.postGuid(guid);
 | 
						|
                    console.debug('process response', resp);
 | 
						|
                     if (resp.data.status === "valid") {
 | 
						|
                            console.log();
 | 
						|
                          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(trimmed, tanggal, waktu);
 | 
						|
                      }
 | 
						|
                      else if (resp.data.status === "exists") {
 | 
						|
                            this.showModal('error', 'SPJ sudah di Scan', (resp && resp.message) ? resp.message : 'Kode SPJ sudah digunakan.', true);
 | 
						|
                      } else
 | 
						|
                      {
 | 
						|
                          this.showModal('error', 'SPJ Tidak Ditemukan', (resp && resp.message) ? resp.message : 'Kode SPJ tidak ditemukan dalam database.', true);
 | 
						|
                      }
 | 
						|
                } catch (xhrOrErr) {
 | 
						|
                    let message = '';
 | 
						|
                    if (xhrOrErr && xhrOrErr.responseJSON) {
 | 
						|
                        message = xhrOrErr.responseJSON.message || 'Terjadi kesalahan saat memproses.';
 | 
						|
                    } else if (xhrOrErr && xhrOrErr.responseText) {
 | 
						|
                        message = xhrOrErr.responseText;
 | 
						|
                    } else if (xhrOrErr instanceof Error) {
 | 
						|
                        message = xhrOrErr.message;
 | 
						|
                    } else {
 | 
						|
                        message = 'Terjadi kesalahan saat memproses scan.';
 | 
						|
                    }
 | 
						|
                    console.error('processScanCode error', xhrOrErr);
 | 
						|
                    this.showModal('error', 'Error', message, true);
 | 
						|
                } finally {
 | 
						|
                    this.originalSpjCode = null;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
 | 
						|
            async retryScan() {
 | 
						|
                this.hideResult();
 | 
						|
                this.hideError();
 | 
						|
                this.hidePermissionMessages();
 | 
						|
                this.detectedCode = null;
 | 
						|
                
 | 
						|
                if (this.isScanning) {
 | 
						|
                    console.log("Scanner sudah aktif, tidak perlu restart");
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
                
 | 
						|
                setTimeout(() => {
 | 
						|
                    this.startScanner();
 | 
						|
                }, 500);
 | 
						|
            }
 | 
						|
 | 
						|
            handleManualSubmit(e) {
 | 
						|
                e.preventDefault();
 | 
						|
                
 | 
						|
                const code = $('#manual-barcode-select').val();
 | 
						|
                const selectedData = $('#manual-barcode-select').select2('data')[0];
 | 
						|
                
 | 
						|
                if (!code) {
 | 
						|
                    this.showModal('error', 'Input Kosong', 'Silakan pilih SPJ dari daftar.', true);
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
                
 | 
						|
                if (code.length < 5) {
 | 
						|
                    this.showModal('error', 'Kode Tidak Valid', 'Kode SPJ minimal 5 karakter.', true);
 | 
						|
                    return;
 | 
						|
                }
 | 
						|
                
 | 
						|
                console.log('Selected SPJ:', {
 | 
						|
                    id: selectedData?.id,
 | 
						|
                    spjCode: selectedData?.text,
 | 
						|
                    driver: selectedData?.driverName,
 | 
						|
                    platNomor: selectedData?.platNomor,
 | 
						|
                    nomorPintu: selectedData?.nomorPintu
 | 
						|
                });
 | 
						|
                
 | 
						|
                this.originalSpjCode = selectedData?.text || code;
 | 
						|
                this.processScanCode(code);
 | 
						|
            }
 | 
						|
 | 
						|
            showLoading() {
 | 
						|
                this.loadingDiv.classList.remove('hidden');
 | 
						|
            }
 | 
						|
 | 
						|
            hideLoading() {
 | 
						|
                this.loadingDiv.classList.add('hidden');
 | 
						|
            }
 | 
						|
 | 
						|
            showResult(code) {
 | 
						|
                this.detectedCodeSpan.textContent = code;
 | 
						|
                this.scanResult.classList.remove('hidden');
 | 
						|
            }
 | 
						|
 | 
						|
            hideResult() {
 | 
						|
                this.scanResult.classList.add('hidden');
 | 
						|
            }
 | 
						|
 | 
						|
            showError(message) {
 | 
						|
                document.getElementById('error-text').textContent = message;
 | 
						|
                this.errorMessage.classList.remove('hidden');
 | 
						|
            }
 | 
						|
 | 
						|
            hideError() {
 | 
						|
                this.errorMessage.classList.add('hidden');
 | 
						|
            }
 | 
						|
 | 
						|
            hidePermissionMessages() {
 | 
						|
                this.permissionInfo.classList.add('hidden');
 | 
						|
                this.permissionDenied.classList.add('hidden');
 | 
						|
            }
 | 
						|
 | 
						|
            playSuccessSound() {
 | 
						|
                try {
 | 
						|
                    const audioContext = new (window.AudioContext || window.webkitAudioContext)();
 | 
						|
                    const oscillator = audioContext.createOscillator();
 | 
						|
                    const gainNode = audioContext.createGain();
 | 
						|
                    
 | 
						|
                    oscillator.connect(gainNode);
 | 
						|
                    gainNode.connect(audioContext.destination);
 | 
						|
                    
 | 
						|
                    oscillator.frequency.value = 800;
 | 
						|
                    oscillator.type = 'square';
 | 
						|
                    
 | 
						|
                    gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
 | 
						|
                    gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2);
 | 
						|
                    
 | 
						|
                    oscillator.start(audioContext.currentTime);
 | 
						|
                    oscillator.stop(audioContext.currentTime + 0.2);
 | 
						|
                } 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>',
 | 
						|
                    'warning': '<div class="w-20 h-20 mx-auto bg-yellow-100 rounded-full flex items-center justify-center shadow-lg"><svg class="w-10 h-10 text-yellow-600" 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></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').addClass('flex');
 | 
						|
                
 | 
						|
                $('#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="grid grid-cols-1 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();
 | 
						|
                
 | 
						|
                $('#scan-modal').removeClass('hidden');
 | 
						|
                $('#scan-modal').addClass('flex');
 | 
						|
                
 | 
						|
                $('#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();
 | 
						|
                    this.hideResult();
 | 
						|
                    $('#manual-barcode-select').val(null).trigger('change');
 | 
						|
                    this.resumeAfterDelay(300);
 | 
						|
                }, 2000);
 | 
						|
            }
 | 
						|
 | 
						|
            showAlreadyScannedModal(code) {
 | 
						|
                const warningIcon = `
 | 
						|
                    <div class="w-28 h-28 mx-auto bg-gradient-to-r from-yellow-400 to-amber-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="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>
 | 
						|
                    </div>
 | 
						|
                `;
 | 
						|
                
 | 
						|
                const warningContent = `
 | 
						|
                    <div class="bg-gradient-to-br from-yellow-50 via-amber-50 to-orange-50 rounded-2xl p-6 border border-yellow-200 shadow-inner">
 | 
						|
                        <div class="flex items-center justify-center mb-4">
 | 
						|
                            <div class="bg-gradient-to-r from-yellow-500 to-amber-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-yellow-100">
 | 
						|
                                <div class="text-center">
 | 
						|
                                    <div class="flex flex-col gap-2 items-center justify-center mb-2">
 | 
						|
                                        <svg class="w-6 h-6 text-yellow-600 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-yellow-700 font-bold text-lg">SPJ SUDAH DISCAN</span>
 | 
						|
                                    </div>
 | 
						|
                                    <p class="text-yellow-600 text-sm mt-2">SPJ ini telah diproses sebelumnya</p>
 | 
						|
                                </div>
 | 
						|
                            </div>
 | 
						|
                        </div>
 | 
						|
                    </div>
 | 
						|
                `;
 | 
						|
 | 
						|
                $('#modal-icon').html(warningIcon);
 | 
						|
                $('#modal-title').html('<span class="text-xl font-bold bg-gradient-to-r from-yellow-600 to-amber-600 bg-clip-text text-transparent">SPJ Sudah Discan</span>');
 | 
						|
                $('#modal-message').html(warningContent);
 | 
						|
                $('#modal-close').hide();
 | 
						|
                
 | 
						|
                $('#scan-modal').removeClass('hidden');
 | 
						|
                $('#scan-modal').addClass('flex');
 | 
						|
                
 | 
						|
                $('#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();
 | 
						|
                    this.resumeAfterDelay(300);
 | 
						|
                }, 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 flex-col gap-2 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>
 | 
						|
                            </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();
 | 
						|
                
 | 
						|
                $('#scan-modal').removeClass('hidden');
 | 
						|
                $('#scan-modal').addClass('flex');
 | 
						|
                
 | 
						|
                $('#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();
 | 
						|
                    this.resumeAfterDelay(300);
 | 
						|
                }, 2000);
 | 
						|
            }
 | 
						|
 | 
						|
           hideModal() {
 | 
						|
                $('#scan-modal').animate({'opacity': '0'}, 200, () => {
 | 
						|
                    $('#scan-modal').addClass('hidden');
 | 
						|
                    $('#scan-modal').removeClass('flex');
 | 
						|
                    $('#scan-modal').css('opacity', '');
 | 
						|
                    $('#scan-modal .bg-white').css('transform', '');
 | 
						|
                    if (this.isProcessing) {
 | 
						|
                        this.resumeAfterDelay(200);
 | 
						|
                    }
 | 
						|
                });
 | 
						|
            }
 | 
						|
 | 
						|
            resumeAfterDelay(ms = 400) {
 | 
						|
                if (this.resumeTimer) {
 | 
						|
                    clearTimeout(this.resumeTimer);
 | 
						|
                }
 | 
						|
                this.resumeTimer = setTimeout(async () => {
 | 
						|
                    try {
 | 
						|
                        if (this.html5QrCode && this.isScanning && typeof this.html5QrCode.resume === 'function') {
 | 
						|
                            await this.html5QrCode.resume();
 | 
						|
                        }
 | 
						|
                    } catch (e) {}
 | 
						|
                    this.isProcessing = false;
 | 
						|
                }, ms);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        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());
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            });
 | 
						|
            
 | 
						|
            initializeSelect2();
 | 
						|
            
 | 
						|
            function waitForLibrary() {
 | 
						|
                if (typeof Html5Qrcode !== 'undefined') {
 | 
						|
                    window.barcodeScanner = new BarcodeScanner();
 | 
						|
                    updateManualFormHandler();
 | 
						|
                } else {
 | 
						|
                    setTimeout(waitForLibrary, 500);
 | 
						|
                }
 | 
						|
            }
 | 
						|
            
 | 
						|
            waitForLibrary();
 | 
						|
        });
 | 
						|
    </script>
 | 
						|
</register-block> |