1571 lines
77 KiB
Plaintext
1571 lines
77 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="max-w-sm mx-auto bg-white min-h-screen">
|
|
<div class="bg-cyan-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-cyan-100 rounded-full p-3">
|
|
<i data-lucide="camera" class="w-7 h-7 text-cyan-500"></i>
|
|
</div>
|
|
<h2 class="text-xl font-bold text-cyan-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 mb-4">
|
|
<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 id="scan-success" class="hidden bg-green-50 border border-green-200 rounded-lg p-3 mb-4">
|
|
<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, "Upload Foto Struk"</p>
|
|
</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-cyan-500 hover:bg-cyan-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>
|
|
</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("Struk", "SpjDriver")" 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-cyan-100 rounded-full p-3">
|
|
<i data-lucide="edit-3" class="w-7 h-7 text-cyan-500"></i>
|
|
</div>
|
|
<h2 class="text-xl font-bold text-cyan-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-cyan-300 shadow-sm focus:border-cyan-500 focus:ring-2 focus:ring-cyan-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-cyan-300 shadow-sm focus:border-cyan-500 focus:ring-2 focus:ring-cyan-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-cyan-300 shadow-sm focus:border-cyan-500 focus:ring-2 focus:ring-cyan-200 transition-all duration-150 px-4 py-2"
|
|
placeholder="JAKARTA BARAT"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Waktu Masuk dan Keluar -->
|
|
<div class="grid grid-cols-1 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-cyan-300 shadow-sm focus:border-cyan-500 focus:ring-2 focus:ring-cyan-200 transition-all duration-150 px-4 py-2"
|
|
placeholder="2025-08-04, 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-cyan-300 shadow-sm focus:border-cyan-500 focus:ring-2 focus:ring-cyan-200 transition-all duration-150 px-4 py-2"
|
|
placeholder="2025-08-04, 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-cyan-300 shadow-sm focus:border-cyan-500 focus:ring-2 focus:ring-cyan-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-cyan-300 shadow-sm focus:border-cyan-500 focus:ring-2 focus:ring-cyan-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-cyan-300 shadow-sm focus:border-cyan-500 focus:ring-2 focus:ring-cyan-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-cyan-500 to-cyan-400 text-white py-3 rounded-lg font-semibold shadow hover:from-cyan-600 hover:to-cyan-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@4.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-cyan-500', 'hover:bg-cyan-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 = '';
|
|
|
|
const receiptPatterns = [
|
|
/(\d{2})_(\d{6,})/gi,
|
|
/(\d{2})\s+(\d{6,})/gi,
|
|
/(?:no.*struk|nomor.*struk|receipt)[\s.:]*(\d{6,})/gi,
|
|
/(?:^|\s)(\d{6,10})(?:\s|$)/g,
|
|
];
|
|
|
|
for (const line of lines) {
|
|
console.log(`Processing line for receipt: "${line}"`);
|
|
|
|
const monthUnderscoreMatch = line.match(/(\d{2})_(\d{6,})/);
|
|
if (monthUnderscoreMatch && monthUnderscoreMatch[2]) {
|
|
receiptNumber = monthUnderscoreMatch[2];
|
|
console.log(`Found receipt number: ${receiptNumber} using month-underscore pattern (removed prefix: ${monthUnderscoreMatch[1]}_)`);
|
|
break;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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) {
|
|
console.log('No truck number found in context lines, trying all lines...');
|
|
for (const line of lines) {
|
|
console.log(`Scanning line for truck pattern: "${line}"`);
|
|
|
|
if (line.match(/[A-Z]\s*\d+\s*[A-Z]{2,3}/i)) {
|
|
console.log('Potential truck pattern found:', 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 general pattern`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (truckNumber) break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!truckNumber) {
|
|
console.log('Still no truck number, trying loose patterns...');
|
|
for (const line of lines) {
|
|
const looseMatch = line.match(/([A-Z]{1,2})\s*(\d{3,4})\s*([A-Z]{2,3})/i);
|
|
if (looseMatch && looseMatch[1] && looseMatch[2] && looseMatch[3]) {
|
|
const candidate = `${looseMatch[1].toUpperCase()} ${looseMatch[2]} ${looseMatch[3].toUpperCase()}`;
|
|
console.log(`Found potential truck number with loose pattern: "${candidate}"`);
|
|
|
|
if (looseMatch[1].length <= 2 &&
|
|
looseMatch[2].length >= 3 && looseMatch[2].length <= 4 &&
|
|
looseMatch[3].length >= 2 && looseMatch[3].length <= 3) {
|
|
truckNumber = candidate;
|
|
console.log(`Accepted truck number: "${truckNumber}"`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('Truck detection results:', { truckNumber });
|
|
|
|
console.log('Truck detection result:', truckNumber);
|
|
|
|
const assignmentPatterns = [
|
|
/(?:penugasan|assignment)[\s.:]*([A-Z\s]+?)(?:\n|$)/gi,
|
|
/(JAKARTA\s+\w+)/gi, // Specific pattern for Jakarta areas
|
|
/(BANDUNG|SURABAYA|MEDAN|SEMARANG|PALEMBANG|MAKASSAR)[\s\w]*/gi,
|
|
];
|
|
|
|
for (const line of lines) {
|
|
console.log(`Processing line for assignment: "${line}"`);
|
|
|
|
if (line.toLowerCase().includes('penugasan')) {
|
|
console.log('Found penugasan line:', line);
|
|
|
|
const assignmentMatch = line.match(/penugasan\s*:\s*(.+)/i);
|
|
if (assignmentMatch && assignmentMatch[1]) {
|
|
assignment = assignmentMatch[1].trim();
|
|
console.log(`Found assignment: ${assignment}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (line.toUpperCase().includes('JAKARTA') && line.toUpperCase().includes('BARAT')) {
|
|
assignment = 'JAKARTA BARAT';
|
|
console.log(`Found assignment: ${assignment} from Jakarta Barat line`);
|
|
break;
|
|
}
|
|
|
|
for (const pattern of assignmentPatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1]) {
|
|
assignment = match[1].trim();
|
|
console.log(`Found assignment: ${assignment} using pattern: ${pattern}`);
|
|
break;
|
|
}
|
|
}
|
|
if (assignment) break;
|
|
}
|
|
|
|
console.log('Assignment detection result:', assignment);
|
|
|
|
// Enhanced time patterns
|
|
const timePatterns = [
|
|
/(\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
|
|
/(\d{4}-\d{1,2}-\d{1,2},\s*\d{1,2}:\d{2}:\d{2})/gi, // 2025-08-04, 08:13:51
|
|
/(\d{4}\/\d{1,2}\/\d{1,2}\s+\d{1,2}:\d{2}:\d{2})/gi, // 2025/08/04 08:13:51
|
|
];
|
|
|
|
const convertToStandardFormat = (dateTimeString) => {
|
|
if (!dateTimeString) return '';
|
|
|
|
console.log('Converting date string:', dateTimeString);
|
|
|
|
const monthNamePattern = /(\d{1,2})\s+(\w{3})\s+(\d{4})[,\s]+(\d{1,2}:\d{2}:\d{2})/i;
|
|
const monthNameMatch = dateTimeString.match(monthNamePattern);
|
|
if (monthNameMatch) {
|
|
const day = monthNameMatch[1].padStart(2, '0');
|
|
const monthName = monthNameMatch[2].toLowerCase();
|
|
const year = monthNameMatch[3];
|
|
const time = monthNameMatch[4];
|
|
|
|
const monthMap = {
|
|
'jan': '01', 'feb': '02', 'mar': '03', 'apr': '04',
|
|
'may': '05', 'jun': '06', 'jul': '07', 'aug': '08',
|
|
'sep': '09', 'oct': '10', 'nov': '11', 'dec': '12'
|
|
};
|
|
|
|
const month = monthMap[monthName] || '01';
|
|
const converted = `${year}-${month}-${day}, ${time}`;
|
|
console.log('Converted month name format:', converted);
|
|
return converted;
|
|
}
|
|
|
|
const ddmmyyyyPattern = /(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2}:\d{2}:\d{2})/;
|
|
const ddmmyyyyMatch = dateTimeString.match(ddmmyyyyPattern);
|
|
if (ddmmyyyyMatch) {
|
|
const day = ddmmyyyyMatch[1].padStart(2, '0');
|
|
const month = ddmmyyyyMatch[2].padStart(2, '0');
|
|
const year = ddmmyyyyMatch[3];
|
|
const time = ddmmyyyyMatch[4];
|
|
const converted = `${year}-${month}-${day}, ${time}`;
|
|
console.log('Converted DD/MM/YYYY format:', converted);
|
|
return converted;
|
|
}
|
|
|
|
const ddmmyyyyDashPattern = /(\d{1,2})-(\d{1,2})-(\d{4})\s+(\d{1,2}:\d{2}:\d{2})/;
|
|
const ddmmyyyyDashMatch = dateTimeString.match(ddmmyyyyDashPattern);
|
|
if (ddmmyyyyDashMatch) {
|
|
const day = ddmmyyyyDashMatch[1].padStart(2, '0');
|
|
const month = ddmmyyyyDashMatch[2].padStart(2, '0');
|
|
const year = ddmmyyyyDashMatch[3];
|
|
const time = ddmmyyyyDashMatch[4];
|
|
const converted = `${year}-${month}-${day}, ${time}`;
|
|
console.log('Converted DD-MM-YYYY format:', converted);
|
|
return converted;
|
|
}
|
|
|
|
const yyyymmddSlashPattern = /(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(\d{1,2}:\d{2}:\d{2})/;
|
|
const yyyymmddSlashMatch = dateTimeString.match(yyyymmddSlashPattern);
|
|
if (yyyymmddSlashMatch) {
|
|
const year = yyyymmddSlashMatch[1];
|
|
const month = yyyymmddSlashMatch[2].padStart(2, '0');
|
|
const day = yyyymmddSlashMatch[3].padStart(2, '0');
|
|
const time = yyyymmddSlashMatch[4];
|
|
const converted = `${year}-${month}-${day}, ${time}`;
|
|
console.log('Converted YYYY/MM/DD format:', converted);
|
|
return converted;
|
|
}
|
|
|
|
const standardPattern = /(\d{4}-\d{1,2}-\d{1,2}),?\s*(\d{1,2}:\d{2}:\d{2})/;
|
|
const standardMatch = dateTimeString.match(standardPattern);
|
|
if (standardMatch) {
|
|
const datePart = standardMatch[1];
|
|
const timePart = standardMatch[2];
|
|
|
|
const [year, month, day] = datePart.split('-');
|
|
const paddedMonth = month.padStart(2, '0');
|
|
const paddedDay = day.padStart(2, '0');
|
|
|
|
const converted = `${year}-${paddedMonth}-${paddedDay}, ${timePart}`;
|
|
console.log('Standardized YYYY-MM-DD format:', converted);
|
|
return converted;
|
|
}
|
|
|
|
console.log('No date pattern matched, returning original:', dateTimeString);
|
|
return dateTimeString;
|
|
};
|
|
|
|
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);
|
|
for (const pattern of timePatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1]) {
|
|
entryTime = convertToStandardFormat(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 = convertToStandardFormat(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);
|
|
for (const pattern of timePatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1]) {
|
|
exitTime = convertToStandardFormat(match[1].trim());
|
|
console.log(`Found exit time: ${exitTime}`);
|
|
break;
|
|
}
|
|
}
|
|
// Also try to extract after "Keluar : "
|
|
const keluarMatch = line.match(/keluar\s*:\s*(.+)/i);
|
|
if (keluarMatch && keluarMatch[1] && !exitTime) {
|
|
exitTime = convertToStandardFormat(keluarMatch[1].trim());
|
|
console.log(`Found exit time via keluar pattern: ${exitTime}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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];
|
|
console.log(`Found weight in via specific pattern: ${weightIn} 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];
|
|
console.log(`Found weight in via general pattern: ${weightIn} 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];
|
|
console.log(`Found weight out via specific pattern: ${weightOut} 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];
|
|
console.log(`Found weight out via general pattern: ${weightOut} 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];
|
|
console.log(`Found weight nett via specific pattern: ${weightNett} 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];
|
|
console.log(`Found weight nett via general pattern: ${weightNett} kg`);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
// 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> |