1478 lines
73 KiB
Plaintext
1478 lines
73 KiB
Plaintext
@{
|
|
Layout = "~/Views/Admin/Transport/SpjDriver/Shared/_Layout.cshtml";
|
|
ViewData["Title"] = "Submit Struk";
|
|
}
|
|
|
|
@section Styles {
|
|
<link rel="stylesheet" href="@Url.Content("~/driver/css/scanner.css")" asp-append-version="true" />
|
|
}
|
|
|
|
<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">Unggah Struk</h1>
|
|
<div class="w-8"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="px-8 py-4">
|
|
<div class="mb-6">
|
|
<div class="flex flex-col items-center space-y-2 mb-4">
|
|
<div class="bg-orange-100 rounded-full p-3">
|
|
<i data-lucide="camera" class="w-7 h-7 text-orange-500"></i>
|
|
</div>
|
|
<h2 class="text-xl font-bold text-orange-500">Scan Struk Otomatis</h2>
|
|
<p class="text-sm text-gray-500 text-center">Arahkan kamera ke struk atau upload foto struk untuk membaca data secara otomatis.</p>
|
|
|
|
</div>
|
|
|
|
<div id="ocr-processing" class="hidden bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
|
<div class="flex items-center">
|
|
<div class="loading-spinner-small mr-2"></div>
|
|
<span class="text-yellow-800 text-sm">Memproses teks dari struk...</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>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-3 mb-4">
|
|
<button id="start-scanner" type="button" 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 Struk
|
|
</button>
|
|
|
|
<button id="stop-scanner" type="button" class="w-full bg-gray-500 hover:bg-gray-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 class="flex items-center">
|
|
<div class="flex-1 border-t border-gray-200"></div>
|
|
<span class="px-3 text-sm text-gray-500 bg-white">atau</span>
|
|
<div class="flex-1 border-t border-gray-200"></div>
|
|
</div>
|
|
<label for="file-upload" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-medium py-3 px-4 rounded-lg transition-colors cursor-pointer flex items-center justify-center gap-2 upload-label">
|
|
<i class="w-5 h-5" data-lucide="upload"></i>
|
|
Upload Foto Struk
|
|
</label>
|
|
<input
|
|
type="file"
|
|
id="file-upload"
|
|
accept="image/*"
|
|
class="hidden"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-2 text-center">
|
|
Pilih foto struk dari galeri atau ambil foto baru
|
|
</p>
|
|
|
|
|
|
<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 id="scan-success" class="hidden bg-green-50 border border-green-200 rounded-lg p-3">
|
|
<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 font-medium">Data struk berhasil diisi otomatis!</span>
|
|
</div>
|
|
<p class="text-green-700 text-sm mt-1">Scanning dihentikan. Periksa form di bawah dan lengkapi data jika diperlukan.</p>
|
|
<p class="text-green-700 text-xs mt-1">💡 Untuk scan ulang, klik "Mulai Scan Struk" atau "Upload Foto Struk"</p>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center my-6">
|
|
<div class="flex-1 border-t border-gray-200"></div>
|
|
<span class="px-3 text-sm text-gray-500 bg-white">Hasil Scan</span>
|
|
<div class="flex-1 border-t border-gray-200"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<form action="@Url.Action("ProcessStruk", "Submit")" method="post" class="px-8 py-4 space-y-6 bg-white">
|
|
<div class="flex flex-col items-center space-y-2">
|
|
<div class="bg-orange-100 rounded-full p-3">
|
|
<i data-lucide="edit-3" class="w-7 h-7 text-orange-500"></i>
|
|
</div>
|
|
<h2 class="text-xl font-bold text-orange-500">Data Struk</h2>
|
|
<p class="text-sm text-gray-500 text-center">Periksa dan lengkapi data struk sebelum submit.</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4">
|
|
<!-- Nomor Struk -->
|
|
<div>
|
|
<label for="NomorStruk" class="block text-sm font-medium text-gray-700 mb-1">Nomor Struk</label>
|
|
<input
|
|
type="text"
|
|
id="NomorStruk"
|
|
name="NomorStruk"
|
|
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
|
|
required
|
|
placeholder="8001441"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">Nomor tanpa prefix (contoh: 8001441)</p>
|
|
</div>
|
|
|
|
<!-- Nomor Polisi -->
|
|
<div>
|
|
<label for="NomorPolisi" class="block text-sm font-medium text-gray-700 mb-1">Nomor Polisi</label>
|
|
<input
|
|
type="text"
|
|
id="NomorPolisi"
|
|
name="NomorPolisi"
|
|
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
|
|
placeholder="B 9125 PJA"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Penugasan -->
|
|
<div>
|
|
<label for="Penugasan" class="block text-sm font-medium text-gray-700 mb-1">Penugasan</label>
|
|
<input
|
|
type="text"
|
|
id="Penugasan"
|
|
name="Penugasan"
|
|
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
|
|
placeholder="JAKARTA BARAT"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Waktu Masuk dan Keluar -->
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label for="WaktuMasuk" class="block text-sm font-medium text-gray-700 mb-1">Masuk</label>
|
|
<input
|
|
type="text"
|
|
id="WaktuMasuk"
|
|
name="WaktuMasuk"
|
|
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
|
|
placeholder="04 Aug 2025, 08:13:51"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label for="WaktuKeluar" class="block text-sm font-medium text-gray-700 mb-1">Keluar</label>
|
|
<input
|
|
type="text"
|
|
id="WaktuKeluar"
|
|
name="WaktuKeluar"
|
|
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
|
|
placeholder="04 Aug 2025, 14:35:10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Berat -->
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label for="BeratMasuk" class="block text-sm font-medium text-gray-700 mb-1">Berat Masuk (kg)</label>
|
|
<input
|
|
type="number"
|
|
id="BeratMasuk"
|
|
name="BeratMasuk"
|
|
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
|
|
placeholder="23280"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label for="BeratKeluar" class="block text-sm font-medium text-gray-700 mb-1">Berat Keluar (kg)</label>
|
|
<input
|
|
type="number"
|
|
id="BeratKeluar"
|
|
name="BeratKeluar"
|
|
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
|
|
placeholder="13540"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Berat Nett -->
|
|
<div>
|
|
<label for="BeratNett" class="block text-sm font-medium text-gray-700 mb-1">Berat Nett (kg)</label>
|
|
<input
|
|
type="number"
|
|
id="BeratNett"
|
|
name="BeratNett"
|
|
class="mt-1 block w-full rounded-lg border border-orange-300 shadow-sm focus:border-orange-500 focus:ring-2 focus:ring-orange-200 transition-all duration-150 px-4 py-2"
|
|
required
|
|
placeholder="9740"
|
|
/>
|
|
<p class="text-xs text-gray-500 mt-1">Berat Nett wajib diisi</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button type="submit" class="w-full bg-gradient-to-r from-orange-500 to-orange-400 text-white py-3 rounded-lg font-semibold shadow hover:from-orange-600 hover:to-orange-500 transition-all duration-150 flex items-center justify-center gap-2">
|
|
<i data-lucide="send" class="w-5 h-5"></i>
|
|
Submit Data Struk
|
|
</button>
|
|
</form>
|
|
|
|
<partial name="~/Views/Admin/Transport/SpjDriver/Shared/Components/_Navigation.cshtml" />
|
|
|
|
</div>
|
|
|
|
<register-block dynamic-section="scripts" key="jsSubmitStruk">
|
|
<script src="https://cdn.jsdelivr.net/npm/tesseract.js@5.1.1/dist/tesseract.min.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const nomorStrukInput = document.getElementById('NomorStruk');
|
|
const nomorPolisiInput = document.getElementById('NomorPolisi');
|
|
const penugasanInput = document.getElementById('Penugasan');
|
|
const waktuMasukInput = document.getElementById('WaktuMasuk');
|
|
const waktuKeluarInput = document.getElementById('WaktuKeluar');
|
|
const beratMasukInput = document.getElementById('BeratMasuk');
|
|
const beratKeluarInput = document.getElementById('BeratKeluar');
|
|
const beratNettInput = document.getElementById('BeratNett');
|
|
|
|
nomorStrukInput.addEventListener('input', function() {
|
|
this.value = this.value.replace(/[^0-9]/g, '');
|
|
});
|
|
|
|
beratMasukInput.addEventListener('input', function() {
|
|
this.value = this.value.replace(/[^0-9]/g, '');
|
|
});
|
|
|
|
beratKeluarInput.addEventListener('input', function() {
|
|
this.value = this.value.replace(/[^0-9]/g, '');
|
|
});
|
|
|
|
beratNettInput.addEventListener('input', function() {
|
|
this.value = this.value.replace(/[^0-9]/g, '');
|
|
});
|
|
|
|
class ReceiptScanner {
|
|
constructor() {
|
|
this.isScanning = false;
|
|
this.stream = null;
|
|
this.video = null;
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
this.detectedData = {
|
|
receiptNumber: '',
|
|
truckNumber: '',
|
|
assignment: '',
|
|
entryTime: '',
|
|
exitTime: '',
|
|
weightIn: '',
|
|
weightOut: '',
|
|
weightNett: ''
|
|
};
|
|
this.ocrTimeout = null;
|
|
this.initializeElements();
|
|
this.bindEvents();
|
|
this.checkBrowserSupport();
|
|
}
|
|
|
|
initializeElements() {
|
|
console.log('Initializing elements...');
|
|
|
|
this.startBtn = document.getElementById('start-scanner');
|
|
this.stopBtn = document.getElementById('stop-scanner');
|
|
this.loadingDiv = document.getElementById('loading-scanner');
|
|
this.ocrProcessing = document.getElementById('ocr-processing');
|
|
this.ocrResult = document.getElementById('ocr-result');
|
|
this.permissionInfo = document.getElementById('permission-info');
|
|
this.permissionDenied = document.getElementById('permission-denied');
|
|
this.scanSuccess = document.getElementById('scan-success');
|
|
|
|
this.scannerContainer = document.getElementById('scanner-container');
|
|
this.fileUpload = document.getElementById('file-upload');
|
|
|
|
console.log('Elements found:', {
|
|
startBtn: !!this.startBtn,
|
|
stopBtn: !!this.stopBtn,
|
|
loadingDiv: !!this.loadingDiv,
|
|
ocrProcessing: !!this.ocrProcessing,
|
|
permissionInfo: !!this.permissionInfo,
|
|
permissionDenied: !!this.permissionDenied,
|
|
scanSuccess: !!this.scanSuccess,
|
|
scannerContainer: !!this.scannerContainer,
|
|
fileUpload: !!this.fileUpload
|
|
});
|
|
|
|
if (!this.fileUpload) {
|
|
console.error('CRITICAL: File upload input not found! Looking for #file-upload');
|
|
console.log('Available file inputs:', document.querySelectorAll('input[type="file"]'));
|
|
console.log('All elements with file-upload in ID:', document.querySelectorAll('[id*="file-upload"]'));
|
|
console.log('DOM ready state:', document.readyState);
|
|
|
|
const altFileInput = document.querySelector('input[type="file"]');
|
|
if (altFileInput) {
|
|
console.log('Found file input via type selector:', altFileInput);
|
|
this.fileUpload = altFileInput;
|
|
}
|
|
}
|
|
|
|
if (!this.scannerContainer) {
|
|
console.error('CRITICAL: Scanner container not found! Looking for #scanner-container');
|
|
console.log('Available containers:', document.querySelectorAll('[id*="scanner"]'));
|
|
}
|
|
}
|
|
|
|
bindEvents() {
|
|
console.log('Binding events...');
|
|
|
|
if (this.startBtn) {
|
|
this.startBtn.addEventListener('click', () => this.startScanner());
|
|
console.log('Start scanner button bound');
|
|
} else {
|
|
console.error('Start scanner button not found!');
|
|
}
|
|
|
|
if (this.stopBtn) {
|
|
this.stopBtn.addEventListener('click', () => this.stopScanner());
|
|
console.log('Stop scanner button bound');
|
|
} else {
|
|
console.error('Stop scanner button not found!');
|
|
}
|
|
|
|
if (this.fileUpload) {
|
|
console.log('File upload element found, binding change event...');
|
|
this.fileUpload.addEventListener('change', (e) => {
|
|
console.log('File upload change event triggered!');
|
|
this.handleFileUpload(e);
|
|
});
|
|
console.log('File upload input bound successfully');
|
|
} else {
|
|
console.error('CRITICAL: File upload input not found! Element ID: file-upload');
|
|
const fileInput = document.querySelector('#file-upload');
|
|
if (fileInput) {
|
|
console.log('Found file input via querySelector, binding now...');
|
|
this.fileUpload = fileInput;
|
|
this.fileUpload.addEventListener('change', (e) => {
|
|
console.log('File upload change event triggered via querySelector!');
|
|
this.handleFileUpload(e);
|
|
});
|
|
} else {
|
|
console.error('File upload input still not found via querySelector');
|
|
}
|
|
}
|
|
}
|
|
|
|
checkBrowserSupport() {
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
this.disableScanner('Browser Tidak Didukung');
|
|
return;
|
|
}
|
|
|
|
if (location.protocol !== 'https:' && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') {
|
|
this.showWarning('Scanner berfungsi optimal dengan koneksi HTTPS yang aman.');
|
|
}
|
|
}
|
|
|
|
disableScanner(message) {
|
|
this.startBtn.disabled = true;
|
|
this.startBtn.innerHTML = `<i class="w-5 h-5 inline mr-2" data-lucide="x-circle"></i>${message}`;
|
|
this.startBtn.classList.remove('bg-orange-500', 'hover:bg-orange-600');
|
|
this.startBtn.classList.add('bg-gray-400', 'cursor-not-allowed');
|
|
}
|
|
|
|
async startScanner() {
|
|
try {
|
|
this.showLoading();
|
|
this.hideMessages();
|
|
this.permissionInfo.classList.remove('hidden');
|
|
|
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
video: {
|
|
facingMode: 'environment',
|
|
width: { ideal: 1280 },
|
|
height: { ideal: 720 }
|
|
}
|
|
});
|
|
|
|
this.setupVideoElement();
|
|
this.isScanning = true;
|
|
this.startBtn.classList.add('hidden');
|
|
this.stopBtn.classList.remove('hidden');
|
|
this.hideLoading();
|
|
this.hideMessages();
|
|
|
|
this.startContinuousCapture();
|
|
|
|
if (this.ocrTimeout) clearTimeout(this.ocrTimeout);
|
|
this.ocrTimeout = setTimeout(() => {
|
|
if (this.isScanning && !this.hasDetectedData()) {
|
|
this.stopScanner();
|
|
this.showError('Gagal scan struk. Silakan input data secara manual.');
|
|
}
|
|
}, 30000);
|
|
|
|
} catch (error) {
|
|
this.handleCameraError(error);
|
|
this.hideLoading();
|
|
}
|
|
}
|
|
|
|
hasDetectedData() {
|
|
const d = this.detectedData;
|
|
return d && (d.receiptNumber || d.truckNumber || d.assignment || d.entryTime || d.exitTime || d.weightIn || d.weightOut || d.weightNett);
|
|
}
|
|
|
|
setupVideoElement() {
|
|
this.video = document.createElement('video');
|
|
this.video.autoplay = true;
|
|
this.video.playsInline = true;
|
|
this.video.muted = true;
|
|
this.video.srcObject = this.stream;
|
|
|
|
this.video.className = 'w-full h-full object-cover rounded-lg';
|
|
|
|
this.scannerContainer.innerHTML = '';
|
|
this.scannerContainer.appendChild(this.video);
|
|
|
|
this.canvas = document.createElement('canvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
}
|
|
|
|
startContinuousCapture() {
|
|
const captureInterval = setInterval(() => {
|
|
if (!this.isScanning) {
|
|
clearInterval(captureInterval);
|
|
return;
|
|
}
|
|
this.captureAndProcessFrame();
|
|
}, 3000);
|
|
}
|
|
|
|
async captureAndProcessFrame() {
|
|
if (!this.video || !this.canvas || !this.isScanning) return;
|
|
|
|
try {
|
|
this.canvas.width = this.video.videoWidth;
|
|
this.canvas.height = this.video.videoHeight;
|
|
|
|
this.ctx.drawImage(this.video, 0, 0);
|
|
|
|
this.canvas.toBlob(async (blob) => {
|
|
await this.processImageWithOCR(blob);
|
|
}, 'image/jpeg', 0.8);
|
|
|
|
} catch (error) {
|
|
console.error('Error capturing frame:', error);
|
|
}
|
|
}
|
|
|
|
setupVideoElement() {
|
|
this.video = document.createElement('video');
|
|
this.video.autoplay = true;
|
|
this.video.playsInline = true;
|
|
this.video.muted = true;
|
|
this.video.srcObject = this.stream;
|
|
|
|
this.video.className = 'w-full h-full object-cover rounded-lg';
|
|
|
|
this.scannerContainer.innerHTML = '';
|
|
this.scannerContainer.appendChild(this.video);
|
|
|
|
this.canvas = document.createElement('canvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
}
|
|
|
|
startContinuousCapture() {
|
|
const captureInterval = setInterval(() => {
|
|
if (!this.isScanning) {
|
|
clearInterval(captureInterval);
|
|
return;
|
|
}
|
|
this.captureAndProcessFrame();
|
|
}, 3000);
|
|
}
|
|
|
|
async captureAndProcessFrame() {
|
|
if (!this.video || !this.canvas || !this.isScanning) return;
|
|
|
|
try {
|
|
this.canvas.width = this.video.videoWidth;
|
|
this.canvas.height = this.video.videoHeight;
|
|
|
|
this.ctx.drawImage(this.video, 0, 0);
|
|
|
|
this.canvas.toBlob(async (blob) => {
|
|
await this.processImageWithOCR(blob);
|
|
}, 'image/jpeg', 0.8);
|
|
|
|
} catch (error) {
|
|
console.error('Error capturing frame:', error);
|
|
}
|
|
}
|
|
|
|
async processImageWithOCR(imageInput) {
|
|
try {
|
|
this.showOcrProcessing();
|
|
|
|
const { data: { text } } = await Tesseract.recognize(
|
|
imageInput,
|
|
'ind+eng',
|
|
{
|
|
logger: m => {
|
|
if (m.status === 'recognizing text') {
|
|
}
|
|
},
|
|
tessedit_pageseg_mode: Tesseract.PSM.AUTO,
|
|
tessedit_char_whitelist: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz :.,/-_()kg',
|
|
}
|
|
);
|
|
|
|
await this.extractDataFromText(text);
|
|
this.hideOcrProcessing();
|
|
|
|
} catch (error) {
|
|
this.hideOcrProcessing();
|
|
this.showError('Gagal membaca teks dari gambar. Coba dengan pencahayaan yang lebih baik.');
|
|
}
|
|
}
|
|
|
|
async extractDataFromText(text) {
|
|
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
|
|
let receiptNumber = '';
|
|
let truckNumber = '';
|
|
let assignment = '';
|
|
let entryTime = '';
|
|
let exitTime = '';
|
|
let weightIn = '';
|
|
let weightOut = '';
|
|
let weightNett = '';
|
|
|
|
// Enhanced receipt number detection - prioritize month prefix patterns
|
|
const receiptPatterns = [
|
|
/(\d{2})_(\d{6,})/gi, // "08_7999566" - underscore pattern (highest priority)
|
|
/(\d{2})\s+(\d{6,})/gi, // "08 7999566" - space pattern
|
|
/(?:no.*struk|nomor.*struk|receipt)[\s.:]*(\d{6,})/gi, // Context-based pattern
|
|
/(?:^|\s)(\d{6,10})(?:\s|$)/g, // Standalone number pattern
|
|
];
|
|
|
|
for (const line of lines) {
|
|
console.log(`Processing line for receipt: "${line}"`);
|
|
|
|
// Pattern 1: Month_Number format (08_7999566) - HIGHEST PRIORITY
|
|
const monthUnderscoreMatch = line.match(/(\d{2})_(\d{6,})/);
|
|
if (monthUnderscoreMatch && monthUnderscoreMatch[2]) {
|
|
receiptNumber = monthUnderscoreMatch[2]; // Take ONLY the number after underscore
|
|
console.log(`Found receipt number: ${receiptNumber} using month-underscore pattern (removed prefix: ${monthUnderscoreMatch[1]}_)`);
|
|
break;
|
|
}
|
|
|
|
// Pattern 2: Month Number format (08 7999566)
|
|
const monthSpaceMatch = line.match(/(\d{2})\s+(\d{6,})/);
|
|
if (monthSpaceMatch && monthSpaceMatch[2]) {
|
|
receiptNumber = monthSpaceMatch[2]; // Take ONLY the number after space
|
|
console.log(`Found receipt number: ${receiptNumber} using month-space pattern (removed prefix: ${monthSpaceMatch[1]} )`);
|
|
break;
|
|
}
|
|
|
|
// Pattern 3: Context-based patterns (fallback)
|
|
for (const pattern of receiptPatterns.slice(2)) {
|
|
const matches = [...line.matchAll(pattern)];
|
|
for (const match of matches) {
|
|
if (match[1] && match[1].length >= 6) {
|
|
receiptNumber = match[1];
|
|
console.log(`Found receipt number: ${receiptNumber} using context pattern: ${pattern}`);
|
|
break;
|
|
}
|
|
}
|
|
if (receiptNumber) break;
|
|
}
|
|
if (receiptNumber) break;
|
|
}
|
|
|
|
const truckPatterns = [
|
|
/([A-Z]\s+\d{1,4}\s+[A-Z]{2,3})/gi, // "B 9125 PJA" (with spaces)
|
|
/([A-Z]\d{1,4}\s+[A-Z]{2,3})/gi, // "B9125 PJA" (no space before number)
|
|
/([A-Z]{1,2}\s*\d{1,4}\s*[A-Z]{1,3})/gi, // Flexible spacing
|
|
|
|
/(?:no.*pol|nopol|nomor.*polisi|no.*truk|nomor.*truk)[\s.:]*([A-Z]{1,2}\s*\d{1,4}\s*[A-Z]{1,3})/gi,
|
|
|
|
/([A-Z]{1,2}\d{3,4}[A-Z]{2,3})/gi, // No spaces at all
|
|
|
|
/([A-Z])\s+(\d{3,4})\s+([A-Z]{2,3})/gi, // Separate capture groups
|
|
];
|
|
|
|
for (const line of lines) {
|
|
const lowerLine = line.toLowerCase();
|
|
console.log(`Processing line for truck: "${line}"`);
|
|
|
|
if (lowerLine.includes('nopol') ||
|
|
lowerLine.includes('no pol') ||
|
|
lowerLine.includes('nomor polisi') ||
|
|
lowerLine.includes('no truk') ||
|
|
lowerLine.includes('nomor truk') ||
|
|
lowerLine.includes('polisi')) {
|
|
console.log('Found truck context line:', line);
|
|
|
|
for (const pattern of truckPatterns) {
|
|
const match = line.match(pattern);
|
|
if (match) {
|
|
let foundTruck = '';
|
|
|
|
if (match.length === 4 && match[1] && match[2] && match[3]) {
|
|
foundTruck = `${match[1]} ${match[2]} ${match[3]}`;
|
|
} else if (match[1]) {
|
|
foundTruck = match[1].trim();
|
|
}
|
|
|
|
if (foundTruck) {
|
|
foundTruck = foundTruck.replace(/([A-Z])(\d+)(\s+[A-Z]{2,3})/g, '$1 $2$3');
|
|
foundTruck = foundTruck.replace(/([A-Z]{1,2})(\d{3,4})([A-Z]{2,3})/g, '$1 $2 $3');
|
|
foundTruck = foundTruck.replace(/\s+/g, ' ').trim();
|
|
|
|
if (foundTruck.match(/^[A-Z]{1,2}\s+\d{3,4}\s+[A-Z]{2,3}$/)) {
|
|
truckNumber = foundTruck;
|
|
console.log(`Found truck number: "${truckNumber}" using context pattern`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (truckNumber) break;
|
|
}
|
|
}
|
|
|
|
if (!truckNumber) {
|
|
// Ambil semua karakter setelah label "No Truk" tanpa filter khusus
|
|
for (const line of lines) {
|
|
if (line.toLowerCase().includes('no truk')) {
|
|
// Contoh: "No Truk : B 9501 TOQ"
|
|
const match = line.match(/no\s*truk\s*:?\s*(.+)/i);
|
|
if (match && match[1]) {
|
|
truckNumber = match[1].trim();
|
|
console.log(`Found truck number: ${truckNumber} (ambil semua karakter setelah label No Truk)`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Jika tidak ditemukan, fallback ke pattern lama (awalan B)
|
|
if (!truckNumber) {
|
|
for (const line of lines) {
|
|
const match = line.match(/\bB\s*\d{3,5}\s*[A-Z]{2,4}\b/i);
|
|
if (match && match[0]) {
|
|
truckNumber = match[0].trim();
|
|
console.log(`Fallback truck number: ${truckNumber}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Truck detection result:', truckNumber);
|
|
|
|
// Assignment detection - ambil semua karakter setelah label "Penugasan"
|
|
for (const line of lines) {
|
|
console.log(`Processing line for assignment: "${line}"`);
|
|
|
|
if (line.toLowerCase().includes('penugasan')) {
|
|
console.log('Found penugasan line:', line);
|
|
|
|
// Contoh: "Penugasan : JAKARTA TIMUR" atau "Penugasan: UPST DLH"
|
|
const match = line.match(/penugasan\s*:?\s*(.+)/i);
|
|
if (match && match[1]) {
|
|
assignment = match[1].trim();
|
|
console.log(`Found assignment: ${assignment} (ambil semua karakter setelah label Penugasan)`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Assignment detection result:', assignment);
|
|
|
|
// Enhanced time patterns - prioritize YYYY-MM-DD HH:MM:SS format
|
|
const timePatterns = [
|
|
/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/gi, // 2025-08-02 12:35:34 (highest priority)
|
|
/(\d{1,2}\s+\w{3}\s+\d{4},\s*\d{1,2}:\d{2}:\d{2})/gi, // 04 Aug 2025, 08:13:51
|
|
/(\d{1,2}\s+\w{3}\s+\d{4}\s+\d{1,2}:\d{2}:\d{2})/gi, // 04 Aug 2025 08:13:51
|
|
/(\d{1,2}\/\d{1,2}\/\d{4}\s+\d{1,2}:\d{2}:\d{2})/gi, // 04/08/2025 08:13:51
|
|
/(\d{1,2}-\d{1,2}-\d{4}\s+\d{1,2}:\d{2}:\d{2})/gi, // 04-08-2025 08:13:51
|
|
];
|
|
|
|
for (const line of lines) {
|
|
console.log(`Processing line for time: "${line}"`);
|
|
|
|
// Entry time - look for "Masuk :" pattern
|
|
if (line.toLowerCase().includes('masuk') && line.includes(':')) {
|
|
console.log('Found masuk line:', line);
|
|
|
|
// First try to find exact YYYY-MM-DD HH:MM:SS format after "Masuk :"
|
|
const masukMatch = line.match(/masuk\s*:\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/i);
|
|
if (masukMatch && masukMatch[1]) {
|
|
entryTime = masukMatch[1].trim();
|
|
console.log(`Found entry time via masuk pattern: ${entryTime} (format: YYYY-MM-DD HH:MM:SS)`);
|
|
} else {
|
|
// Fallback to general patterns
|
|
for (const pattern of timePatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1]) {
|
|
entryTime = match[1].trim();
|
|
console.log(`Found entry time: ${entryTime}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Also try to extract after "Masuk : "
|
|
const masukMatch = line.match(/masuk\s*:\s*(.+)/i);
|
|
if (masukMatch && masukMatch[1] && !entryTime) {
|
|
entryTime = masukMatch[1].trim();
|
|
console.log(`Found entry time via masuk pattern: ${entryTime}`);
|
|
}
|
|
}
|
|
|
|
// Exit time - look for "Keluar :" pattern
|
|
if (line.toLowerCase().includes('keluar') && line.includes(':')) {
|
|
console.log('Found keluar line:', line);
|
|
|
|
// First try to find exact YYYY-MM-DD HH:MM:SS format after "Keluar :"
|
|
const keluarMatch = line.match(/keluar\s*:\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/i);
|
|
if (keluarMatch && keluarMatch[1]) {
|
|
exitTime = keluarMatch[1].trim();
|
|
console.log(`Found exit time via keluar pattern: ${exitTime} (format: YYYY-MM-DD HH:MM:SS)`);
|
|
} else {
|
|
// Fallback to general patterns
|
|
for (const pattern of timePatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1]) {
|
|
exitTime = match[1].trim();
|
|
console.log(`Found exit time: ${exitTime}`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Time detection results:', { entryTime, exitTime });
|
|
|
|
// Enhanced weight patterns - more specific for Indonesian receipts
|
|
const weightPatterns = [
|
|
/berat\s+masuk\s*:\s*(\d+)\s*kg/gi, // Berat Masuk : 23280 kg
|
|
/berat\s+keluar\s*:\s*(\d+)\s*kg/gi, // Berat Keluar : 13540 kg
|
|
/berat\s+nett?\s*:\s*(\d+)\s*kg/gi, // Berat Nett : 9740 kg
|
|
/:\s*(\d{3,6})\s*kg/gi, // : 23280 kg
|
|
/(\d{3,6})\s*kg/gi, // 23280 kg
|
|
/:\s*(\d{3,6})/g, // : 23280
|
|
];
|
|
|
|
for (const line of lines) {
|
|
const lowerLine = line.toLowerCase();
|
|
console.log(`Processing line for weight: "${line}"`);
|
|
|
|
// Berat Masuk - look for exact pattern "Berat Masuk : 23280 kg"
|
|
if (lowerLine.includes('berat masuk')) {
|
|
console.log('Found berat masuk line:', line);
|
|
|
|
// Try specific pattern first
|
|
const specificMatch = line.match(/berat\s+masuk\s*:\s*(\d+)\s*kg/gi);
|
|
if (specificMatch) {
|
|
const numbers = specificMatch[0].match(/(\d+)/);
|
|
if (numbers && numbers[1]) {
|
|
weightIn = numbers[1]; // Hanya angka, tanpa "kg"
|
|
console.log(`Found weight in via specific pattern: ${weightIn} (removed kg)`);
|
|
}
|
|
} else {
|
|
// Try general patterns
|
|
for (const pattern of weightPatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1] && parseInt(match[1]) > 1000) {
|
|
weightIn = match[1]; // Hanya angka
|
|
console.log(`Found weight in via general pattern: ${weightIn} (removed kg)`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Berat Keluar - look for exact pattern "Berat Keluar : 13540 kg"
|
|
if (lowerLine.includes('berat keluar')) {
|
|
console.log('Found berat keluar line:', line);
|
|
|
|
// Try specific pattern first
|
|
const specificMatch = line.match(/berat\s+keluar\s*:\s*(\d+)\s*kg/gi);
|
|
if (specificMatch) {
|
|
const numbers = specificMatch[0].match(/(\d+)/);
|
|
if (numbers && numbers[1]) {
|
|
weightOut = numbers[1]; // Hanya angka, tanpa "kg"
|
|
console.log(`Found weight out via specific pattern: ${weightOut} (removed kg)`);
|
|
}
|
|
} else {
|
|
// Try general patterns
|
|
for (const pattern of weightPatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1] && parseInt(match[1]) > 1000) {
|
|
weightOut = match[1]; // Hanya angka
|
|
console.log(`Found weight out via general pattern: ${weightOut} (removed kg)`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Berat Nett - look for exact pattern "Berat Nett : 9740 kg"
|
|
if ((lowerLine.includes('berat nett') || lowerLine.includes('berat net'))) {
|
|
console.log('Found berat nett line:', line);
|
|
|
|
// Try specific pattern first
|
|
const specificMatch = line.match(/berat\s+nett?\s*:\s*(\d+)\s*kg/gi);
|
|
if (specificMatch) {
|
|
const numbers = specificMatch[0].match(/(\d+)/);
|
|
if (numbers && numbers[1]) {
|
|
weightNett = numbers[1]; // Hanya angka, tanpa "kg"
|
|
console.log(`Found weight nett via specific pattern: ${weightNett} (removed kg)`);
|
|
}
|
|
} else {
|
|
// Try general patterns
|
|
for (const pattern of weightPatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1] && parseInt(match[1]) > 100) {
|
|
weightNett = match[1]; // Hanya angka
|
|
console.log(`Found weight nett via general pattern: ${weightNett} (removed kg)`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validasi Berat Nett = Berat Masuk - Berat Keluar
|
|
if (weightIn && weightOut && weightNett) {
|
|
const calculatedNett = parseInt(weightIn) - parseInt(weightOut);
|
|
const detectedNett = parseInt(weightNett);
|
|
|
|
console.log(`Weight validation: ${weightIn} - ${weightOut} = ${calculatedNett}, detected: ${detectedNett}`);
|
|
|
|
if (calculatedNett !== detectedNett) {
|
|
console.warn(`WARNING: Berat Nett tidak sesuai! Perhitungan: ${calculatedNett}, Terdeteksi: ${detectedNett}`);
|
|
// Gunakan hasil perhitungan yang benar
|
|
weightNett = calculatedNett.toString();
|
|
console.log(`Using calculated weight nett: ${weightNett}`);
|
|
|
|
// Alert akan ditampilkan di frontend saat apply data
|
|
this.weightValidationError = `Berat Nett tidak sesuai! Perhitungan: ${calculatedNett} kg, Terdeteksi: ${detectedNett} kg. Menggunakan hasil perhitungan.`;
|
|
}
|
|
}
|
|
|
|
console.log('Weight detection results:', { weightIn, weightOut, weightNett });
|
|
|
|
console.log('Final Detected Data before assignment:', {
|
|
receiptNumber, truckNumber, assignment, entryTime, exitTime,
|
|
weightIn, weightOut, weightNett
|
|
}); // Debug log
|
|
|
|
// Update detected data with explicit logging
|
|
this.detectedData = {
|
|
receiptNumber: receiptNumber || '',
|
|
truckNumber: truckNumber || '',
|
|
assignment: assignment || '',
|
|
entryTime: entryTime || '',
|
|
exitTime: exitTime || '',
|
|
weightIn: weightIn || '',
|
|
weightOut: weightOut || '',
|
|
weightNett: weightNett || ''
|
|
};
|
|
|
|
console.log('Final Detected Data after assignment:', this.detectedData);
|
|
|
|
// If we found any data, apply directly to form
|
|
if (receiptNumber || truckNumber || assignment || weightNett) {
|
|
console.log('Data found, applying directly to form');
|
|
this.applyDetectedDataDirectly();
|
|
this.playSuccessSound();
|
|
this.vibrate();
|
|
|
|
// Show success message
|
|
this.showScanSuccess();
|
|
|
|
// Auto-stop scanning after successful detection
|
|
console.log('Auto-stopping scanner after successful data detection');
|
|
setTimeout(() => {
|
|
if (this.isScanning) {
|
|
console.log('Scanner stopped automatically. User can click "Mulai Scan Struk" to scan again.');
|
|
this.stopScanner();
|
|
}
|
|
}, 1000);
|
|
|
|
// Scroll to form to show filled data
|
|
setTimeout(() => {
|
|
const formSection = document.querySelector('form');
|
|
if (formSection) {
|
|
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
}, 1500);
|
|
} else {
|
|
}
|
|
}
|
|
|
|
applyDetectedDataDirectly() {
|
|
console.log(`=== APPLYING DATA DIRECTLY TO FORM ===`);
|
|
console.log('Data to apply:', this.detectedData);
|
|
|
|
// Tampilkan alert jika ada error validasi berat nett
|
|
if (this.weightValidationError) {
|
|
alert(this.weightValidationError);
|
|
this.weightValidationError = null; // Reset error
|
|
}
|
|
|
|
// Get input elements by ID directly
|
|
const nomorStrukInput = document.getElementById('NomorStruk');
|
|
const nomorPolisiInput = document.getElementById('NomorPolisi');
|
|
const penugasanInput = document.getElementById('Penugasan');
|
|
const waktuMasukInput = document.getElementById('WaktuMasuk');
|
|
const waktuKeluarInput = document.getElementById('WaktuKeluar');
|
|
const beratMasukInput = document.getElementById('BeratMasuk');
|
|
const beratKeluarInput = document.getElementById('BeratKeluar');
|
|
const beratNettInput = document.getElementById('BeratNett');
|
|
|
|
let fieldsUpdated = 0;
|
|
|
|
// Apply Receipt Number
|
|
if (this.detectedData.receiptNumber && nomorStrukInput) {
|
|
console.log('Setting receipt number:', this.detectedData.receiptNumber);
|
|
nomorStrukInput.value = this.detectedData.receiptNumber;
|
|
nomorStrukInput.classList.add('auto-filled');
|
|
nomorStrukInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => nomorStrukInput.classList.remove('auto-filled'), 3000);
|
|
fieldsUpdated++;
|
|
}
|
|
|
|
// Apply Truck Number
|
|
if (this.detectedData.truckNumber && nomorPolisiInput) {
|
|
console.log('Setting truck number:', this.detectedData.truckNumber);
|
|
nomorPolisiInput.value = this.detectedData.truckNumber;
|
|
nomorPolisiInput.classList.add('auto-filled');
|
|
nomorPolisiInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => nomorPolisiInput.classList.remove('auto-filled'), 3000);
|
|
fieldsUpdated++;
|
|
}
|
|
|
|
// Apply Assignment
|
|
if (this.detectedData.assignment && penugasanInput) {
|
|
console.log('Setting assignment:', this.detectedData.assignment);
|
|
penugasanInput.value = this.detectedData.assignment;
|
|
penugasanInput.classList.add('auto-filled');
|
|
penugasanInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => penugasanInput.classList.remove('auto-filled'), 3000);
|
|
fieldsUpdated++;
|
|
}
|
|
|
|
// Apply Entry Time
|
|
if (this.detectedData.entryTime && waktuMasukInput) {
|
|
console.log('Setting entry time:', this.detectedData.entryTime);
|
|
waktuMasukInput.value = this.detectedData.entryTime;
|
|
waktuMasukInput.classList.add('auto-filled');
|
|
waktuMasukInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => waktuMasukInput.classList.remove('auto-filled'), 3000);
|
|
fieldsUpdated++;
|
|
}
|
|
|
|
// Apply Exit Time
|
|
if (this.detectedData.exitTime && waktuKeluarInput) {
|
|
console.log('Setting exit time:', this.detectedData.exitTime);
|
|
waktuKeluarInput.value = this.detectedData.exitTime;
|
|
waktuKeluarInput.classList.add('auto-filled');
|
|
waktuKeluarInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => waktuKeluarInput.classList.remove('auto-filled'), 3000);
|
|
fieldsUpdated++;
|
|
}
|
|
|
|
// Apply Weight In
|
|
if (this.detectedData.weightIn && beratMasukInput) {
|
|
console.log('Setting weight in:', this.detectedData.weightIn);
|
|
beratMasukInput.value = this.detectedData.weightIn;
|
|
beratMasukInput.classList.add('auto-filled');
|
|
beratMasukInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => beratMasukInput.classList.remove('auto-filled'), 3000);
|
|
fieldsUpdated++;
|
|
}
|
|
|
|
// Apply Weight Out
|
|
if (this.detectedData.weightOut && beratKeluarInput) {
|
|
console.log('Setting weight out:', this.detectedData.weightOut);
|
|
beratKeluarInput.value = this.detectedData.weightOut;
|
|
beratKeluarInput.classList.add('auto-filled');
|
|
beratKeluarInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => beratKeluarInput.classList.remove('auto-filled'), 3000);
|
|
fieldsUpdated++;
|
|
}
|
|
|
|
// Apply Weight Nett
|
|
if (this.detectedData.weightNett && beratNettInput) {
|
|
console.log('Setting weight nett:', this.detectedData.weightNett);
|
|
beratNettInput.value = this.detectedData.weightNett;
|
|
beratNettInput.classList.add('auto-filled');
|
|
beratNettInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => beratNettInput.classList.remove('auto-filled'), 3000);
|
|
fieldsUpdated++;
|
|
}
|
|
|
|
console.log(`=== DIRECT APPLY COMPLETED: ${fieldsUpdated} fields updated ===`);
|
|
return fieldsUpdated;
|
|
}
|
|
|
|
applyDetectedData() {
|
|
console.log('=== APPLY DETECTED DATA CALLED ===');
|
|
console.log('ApplyDetectedData called with:', this.detectedData);
|
|
|
|
// Get input elements by ID directly
|
|
const nomorStrukInput = document.getElementById('NomorStruk');
|
|
const nomorPolisiInput = document.getElementById('NomorPolisi');
|
|
const penugasanInput = document.getElementById('Penugasan');
|
|
const waktuMasukInput = document.getElementById('WaktuMasuk');
|
|
const waktuKeluarInput = document.getElementById('WaktuKeluar');
|
|
const beratMasukInput = document.getElementById('BeratMasuk');
|
|
const beratKeluarInput = document.getElementById('BeratKeluar');
|
|
const beratNettInput = document.getElementById('BeratNett');
|
|
|
|
console.log('Input elements found:', {
|
|
nomorStrukInput: !!nomorStrukInput,
|
|
nomorPolisiInput: !!nomorPolisiInput,
|
|
penugasanInput: !!penugasanInput,
|
|
waktuMasukInput: !!waktuMasukInput,
|
|
waktuKeluarInput: !!waktuKeluarInput,
|
|
beratMasukInput: !!beratMasukInput,
|
|
beratKeluarInput: !!beratKeluarInput,
|
|
beratNettInput: !!beratNettInput
|
|
});
|
|
|
|
// Debug: Check if elements exist and are visible
|
|
if (nomorStrukInput) {
|
|
console.log('NomorStruk element properties:', {
|
|
id: nomorStrukInput.id,
|
|
type: nomorStrukInput.type,
|
|
disabled: nomorStrukInput.disabled,
|
|
readonly: nomorStrukInput.readOnly,
|
|
currentValue: nomorStrukInput.value
|
|
});
|
|
}
|
|
|
|
let fieldsUpdated = 0;
|
|
|
|
// Apply Receipt Number
|
|
if (this.detectedData.receiptNumber && nomorStrukInput) {
|
|
console.log('Setting receipt number:', this.detectedData.receiptNumber);
|
|
nomorStrukInput.value = this.detectedData.receiptNumber;
|
|
nomorStrukInput.classList.add('auto-filled');
|
|
// Trigger input event to ensure any validation runs
|
|
nomorStrukInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => nomorStrukInput.classList.remove('auto-filled'), 2000);
|
|
fieldsUpdated++;
|
|
console.log('Receipt number field updated. New value:', nomorStrukInput.value);
|
|
}
|
|
|
|
// Apply Truck Number
|
|
if (this.detectedData.truckNumber && nomorPolisiInput) {
|
|
console.log('Setting truck number:', this.detectedData.truckNumber);
|
|
nomorPolisiInput.value = this.detectedData.truckNumber;
|
|
nomorPolisiInput.classList.add('auto-filled');
|
|
nomorPolisiInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => nomorPolisiInput.classList.remove('auto-filled'), 2000);
|
|
fieldsUpdated++;
|
|
console.log('Truck number field updated. New value:', nomorPolisiInput.value);
|
|
} else {
|
|
console.log('Truck number not applied. Data:', this.detectedData.truckNumber, 'Input exists:', !!nomorPolisiInput);
|
|
}
|
|
|
|
// Apply Assignment
|
|
if (this.detectedData.assignment && penugasanInput) {
|
|
console.log('Setting assignment:', this.detectedData.assignment);
|
|
penugasanInput.value = this.detectedData.assignment;
|
|
penugasanInput.classList.add('auto-filled');
|
|
penugasanInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => penugasanInput.classList.remove('auto-filled'), 2000);
|
|
fieldsUpdated++;
|
|
console.log('Assignment field updated. New value:', penugasanInput.value);
|
|
}
|
|
|
|
// Apply Entry Time
|
|
if (this.detectedData.entryTime && waktuMasukInput) {
|
|
console.log('Setting entry time:', this.detectedData.entryTime);
|
|
waktuMasukInput.value = this.detectedData.entryTime;
|
|
waktuMasukInput.classList.add('auto-filled');
|
|
waktuMasukInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => waktuMasukInput.classList.remove('auto-filled'), 2000);
|
|
fieldsUpdated++;
|
|
console.log('Entry time field updated. New value:', waktuMasukInput.value);
|
|
}
|
|
|
|
// Apply Exit Time
|
|
if (this.detectedData.exitTime && waktuKeluarInput) {
|
|
console.log('Setting exit time:', this.detectedData.exitTime);
|
|
waktuKeluarInput.value = this.detectedData.exitTime;
|
|
waktuKeluarInput.classList.add('auto-filled');
|
|
waktuKeluarInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => waktuKeluarInput.classList.remove('auto-filled'), 2000);
|
|
fieldsUpdated++;
|
|
console.log('Exit time field updated. New value:', waktuKeluarInput.value);
|
|
}
|
|
|
|
// Apply Weight In
|
|
if (this.detectedData.weightIn && beratMasukInput) {
|
|
console.log('Setting weight in:', this.detectedData.weightIn);
|
|
beratMasukInput.value = this.detectedData.weightIn;
|
|
beratMasukInput.classList.add('auto-filled');
|
|
beratMasukInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => beratMasukInput.classList.remove('auto-filled'), 2000);
|
|
fieldsUpdated++;
|
|
console.log('Weight in field updated. New value:', beratMasukInput.value);
|
|
}
|
|
|
|
// Apply Weight Out
|
|
if (this.detectedData.weightOut && beratKeluarInput) {
|
|
console.log('Setting weight out:', this.detectedData.weightOut);
|
|
beratKeluarInput.value = this.detectedData.weightOut;
|
|
beratKeluarInput.classList.add('auto-filled');
|
|
beratKeluarInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => beratKeluarInput.classList.remove('auto-filled'), 2000);
|
|
fieldsUpdated++;
|
|
console.log('Weight out field updated. New value:', beratKeluarInput.value);
|
|
}
|
|
|
|
// Apply Weight Nett
|
|
if (this.detectedData.weightNett && beratNettInput) {
|
|
console.log('Setting weight nett:', this.detectedData.weightNett);
|
|
beratNettInput.value = this.detectedData.weightNett;
|
|
beratNettInput.classList.add('auto-filled');
|
|
beratNettInput.dispatchEvent(new Event('input', { bubbles: true }));
|
|
setTimeout(() => beratNettInput.classList.remove('auto-filled'), 2000);
|
|
fieldsUpdated++;
|
|
console.log('Weight nett field updated. New value:', beratNettInput.value);
|
|
}
|
|
|
|
console.log(`=== APPLY COMPLETED: ${fieldsUpdated} fields updated ===`);
|
|
|
|
this.hideOcrResult();
|
|
|
|
// Show success message and scroll to form
|
|
this.showSuccess(`Data dari scan telah diisi ke ${fieldsUpdated} field!`);
|
|
|
|
// Scroll to form section to show the filled data
|
|
const formSection = document.querySelector('form');
|
|
if (formSection) {
|
|
formSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
}
|
|
|
|
// Removed unused functions - auto-fill is now automatic
|
|
|
|
async handleFileUpload(event) {
|
|
console.log('=== FILE UPLOAD TRIGGERED ===');
|
|
console.log('Event:', event);
|
|
console.log('Event target:', event.target);
|
|
console.log('Files:', event.target.files);
|
|
|
|
const file = event.target.files[0];
|
|
console.log('Selected file:', file);
|
|
|
|
if (!file) {
|
|
console.log('No file selected');
|
|
return;
|
|
}
|
|
|
|
console.log('File details:', {
|
|
name: file.name,
|
|
type: file.type,
|
|
size: file.size,
|
|
lastModified: file.lastModified
|
|
});
|
|
|
|
// Validate file type
|
|
if (!file.type.startsWith('image/')) {
|
|
console.error('Invalid file type:', file.type);
|
|
this.showError('Harap pilih file gambar (JPG, PNG, etc.)');
|
|
event.target.value = ''; // Reset input
|
|
return;
|
|
}
|
|
|
|
// Validate file size (max 10MB)
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
console.error('File too large:', file.size);
|
|
this.showError('Ukuran file terlalu besar. Maksimal 10MB.');
|
|
event.target.value = ''; // Reset input
|
|
return;
|
|
}
|
|
|
|
try {
|
|
console.log('Starting file processing...');
|
|
this.hideMessages();
|
|
this.showOcrProcessing();
|
|
|
|
// Stop any active scanning first
|
|
if (this.isScanning) {
|
|
await this.stopScanner();
|
|
}
|
|
|
|
// Show uploaded image in scanner container
|
|
console.log('Displaying uploaded image...');
|
|
this.displayUploadedImage(file);
|
|
|
|
await this.processImageWithOCR(file);
|
|
|
|
} catch (error) {
|
|
console.error('Upload processing error:', error);
|
|
this.hideOcrProcessing();
|
|
this.showError('Gagal memproses gambar yang diupload.');
|
|
} finally {
|
|
// Reset file input for next upload
|
|
event.target.value = '';
|
|
}
|
|
}
|
|
|
|
displayUploadedImage(file) {
|
|
console.log('Displaying uploaded image...');
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
console.log('FileReader loaded, creating image element...');
|
|
const img = document.createElement('img');
|
|
img.src = e.target.result;
|
|
img.className = 'w-full h-full object-contain rounded-lg';
|
|
img.style.backgroundColor = '#f3f4f6';
|
|
img.style.maxHeight = '300px';
|
|
|
|
// Add loading indicator
|
|
img.onload = () => {
|
|
console.log('Image loaded successfully in container');
|
|
};
|
|
|
|
img.onerror = () => {
|
|
console.error('Error loading image in container');
|
|
this.showError('Gagal menampilkan gambar yang diupload.');
|
|
};
|
|
|
|
this.scannerContainer.innerHTML = '';
|
|
this.scannerContainer.appendChild(img);
|
|
console.log('Image added to scanner container');
|
|
};
|
|
reader.onerror = () => {
|
|
console.error('FileReader error');
|
|
this.showError('Gagal membaca file gambar.');
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
async processImageWithOCR(imageInput) {
|
|
try {
|
|
this.showOcrProcessing();
|
|
|
|
const { data: { text } } = await Tesseract.recognize(
|
|
imageInput,
|
|
'ind+eng',
|
|
{
|
|
logger: m => {
|
|
if (m.status === 'recognizing text') {
|
|
}
|
|
},
|
|
tessedit_pageseg_mode: Tesseract.PSM.AUTO,
|
|
tessedit_char_whitelist: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz :.,/-_()kg',
|
|
}
|
|
);
|
|
|
|
await this.extractDataFromText(text);
|
|
this.hideOcrProcessing();
|
|
|
|
} catch (error) {
|
|
this.hideOcrProcessing();
|
|
this.showError('Gagal membaca teks dari gambar. Coba dengan pencahayaan yang lebih baik.');
|
|
}
|
|
}
|
|
|
|
async stopScanner() {
|
|
if (this.stream) {
|
|
this.stream.getTracks().forEach(track => track.stop());
|
|
this.stream = null;
|
|
}
|
|
|
|
this.isScanning = false;
|
|
this.startBtn.classList.remove('hidden');
|
|
this.stopBtn.classList.add('hidden');
|
|
this.hideLoading();
|
|
|
|
// Only hide permission and processing messages
|
|
this.permissionInfo.classList.add('hidden');
|
|
this.permissionDenied.classList.add('hidden');
|
|
this.ocrProcessing.classList.add('hidden');
|
|
// Keep this.ocrResult visible if it contains data
|
|
|
|
if (this.ocrTimeout) {
|
|
clearTimeout(this.ocrTimeout);
|
|
this.ocrTimeout = null;
|
|
}
|
|
|
|
// Reset file upload
|
|
if (this.fileUpload) {
|
|
this.fileUpload.value = '';
|
|
}
|
|
|
|
// Reset scanner container
|
|
this.scannerContainer.innerHTML = `
|
|
<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>
|
|
`;
|
|
this.loadingDiv = document.getElementById('loading-scanner');
|
|
}
|
|
|
|
handleCameraError(error) {
|
|
if (error.name === 'NotAllowedError') {
|
|
this.permissionDenied.classList.remove('hidden');
|
|
} else if (error.name === 'NotFoundError') {
|
|
this.showError('Kamera tidak ditemukan pada perangkat ini.');
|
|
} else if (error.name === 'NotReadableError') {
|
|
this.showError('Kamera sedang digunakan aplikasi lain. Tutup aplikasi lain dan coba lagi.');
|
|
} else {
|
|
this.showError('Gagal mengakses kamera: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Helper methods
|
|
showLoading() {
|
|
this.loadingDiv.classList.remove('hidden');
|
|
}
|
|
|
|
hideLoading() {
|
|
this.loadingDiv.classList.add('hidden');
|
|
}
|
|
|
|
showOcrProcessing() {
|
|
this.ocrProcessing.classList.remove('hidden');
|
|
}
|
|
|
|
hideOcrProcessing() {
|
|
this.ocrProcessing.classList.add('hidden');
|
|
}
|
|
|
|
hideMessages() {
|
|
this.permissionInfo.classList.add('hidden');
|
|
this.permissionDenied.classList.add('hidden');
|
|
this.ocrProcessing.classList.add('hidden');
|
|
this.scanSuccess.classList.add('hidden');
|
|
}
|
|
|
|
showScanSuccess() {
|
|
this.scanSuccess.classList.remove('hidden');
|
|
// Auto-hide after 5 seconds (longer to let user read the instructions)
|
|
setTimeout(() => {
|
|
this.scanSuccess.classList.add('hidden');
|
|
}, 5000);
|
|
}
|
|
|
|
showError(message) {
|
|
// Create temporary error message
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'bg-red-50 border border-red-200 rounded-lg p-3 mt-2';
|
|
errorDiv.innerHTML = `
|
|
<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 text-sm">${message}</span>
|
|
</div>
|
|
`;
|
|
this.scannerContainer.parentNode.insertBefore(errorDiv, this.scannerContainer.nextSibling);
|
|
|
|
setTimeout(() => {
|
|
errorDiv.remove();
|
|
}, 5000);
|
|
}
|
|
|
|
showSuccess(message) {
|
|
// Create temporary success message
|
|
const successDiv = document.createElement('div');
|
|
successDiv.className = 'bg-green-50 border border-green-200 rounded-lg p-3 mt-2';
|
|
successDiv.innerHTML = `
|
|
<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 text-sm">${message}</span>
|
|
</div>
|
|
`;
|
|
this.scannerContainer.parentNode.insertBefore(successDiv, this.scannerContainer.nextSibling);
|
|
|
|
setTimeout(() => {
|
|
successDiv.remove();
|
|
}, 3000);
|
|
}
|
|
|
|
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) {
|
|
// Silent fail
|
|
}
|
|
}
|
|
|
|
vibrate() {
|
|
if ('vibrate' in navigator) {
|
|
navigator.vibrate([200]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize the receipt scanner
|
|
const scanner = new ReceiptScanner();
|
|
|
|
// Additional fallback for file upload if not bound during initialization
|
|
setTimeout(() => {
|
|
const fileUploadFallback = document.getElementById('file-upload');
|
|
if (fileUploadFallback && !fileUploadFallback.hasAttribute('data-bound')) {
|
|
console.log('Setting up fallback file upload listener...');
|
|
fileUploadFallback.addEventListener('change', (e) => {
|
|
console.log('Fallback file upload triggered!');
|
|
scanner.handleFileUpload(e);
|
|
});
|
|
fileUploadFallback.setAttribute('data-bound', 'true');
|
|
}
|
|
}, 1000);
|
|
});
|
|
</script>
|
|
</register-block> |