eSPJ/Views/Admin/Transport/SpjDriverUpst/Submit/Struk.cshtml

756 lines
38 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-upst text-white px-6 pt-10 pb-16 rounded-b-[40px] shadow-lg relative overflow-hidden">
<div class="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -mr-16 -mt-16"></div>
<div class="flex items-center justify-between relative z-10">
<a href="@Url.Action("Index", "Home")" class="w-10 h-10 flex items-center justify-center bg-white/20 hover:bg-white/30 rounded-xl transition-colors">
<i class="w-5 h-5" data-lucide="arrow-left"></i>
</a>
<div class="text-center">
<h1 class="text-lg font-bold tracking-wide uppercase">Unggah Struk</h1>
<p class="text-[10px] text-white/70 font-medium">Upload Struk SPJ</p>
</div>
<img src="@Url.Content("~/driver/upst_white.svg")" alt="UPST Logo" class="absolute top-6 left-8 w-20 h-auto opacity-20">
<div class="w-10"></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-upst rounded-full p-3">
<i data-lucide="camera" class="w-7 h-7 text-white"></i>
</div>
<h2 class="text-xl font-bold text-gray-700">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">AI sedang menganalisis foto 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">AI berhasil membaca data struk!</span>
</div>
<p class="text-green-700 text-sm mt-1">Data telah diisi otomatis. Periksa dan lengkapi jika diperlukan.</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-upst hover:bg-gray-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>
<div id="scanner-active-btns" class="hidden grid-cols-2 gap-2" style="display:none">
<button id="capture-now" type="button" class="bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-4 rounded-lg transition-colors flex items-center justify-center gap-2">
<i class="w-5 h-5" data-lucide="zap"></i>
Capture
</button>
<button id="stop-scanner" type="button" class="bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg transition-colors flex items-center justify-center gap-2">
<i class="w-5 h-5" data-lucide="camera-off"></i>
Stop
</button>
</div>
@* <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-upst 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 id="struk-form" action="@Url.Action("ProcessStruk", "Submit")" method="post" enctype="multipart/form-data" class="px-8 py-4 space-y-6 bg-white">
<input type="file" id="FotoStruk" name="FotoStruk" accept="image/*" class="hidden">
<div id="foto-struk-preview" class="hidden">
<div class="flex items-center justify-between mb-1">
<label class="block text-sm font-medium text-gray-700">Foto Struk <span class="text-red-500">*</span></label>
<span class="text-xs text-green-600 font-medium flex items-center gap-1">
<i data-lucide="check-circle" class="w-3 h-3"></i> Tersimpan
</span>
</div>
<img id="foto-struk-img" class="w-full rounded-lg border border-gray-200 max-h-48 object-contain bg-gray-50" alt="Foto Struk" />
</div>
<div id="foto-struk-empty" class="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<div class="flex items-center gap-2">
<i data-lucide="image" class="w-4 h-4 text-yellow-600 shrink-0"></i>
<span class="text-yellow-800 text-sm font-medium">Foto struk belum diambil</span>
</div>
<p class="text-yellow-700 text-xs mt-1">Gunakan kamera atau upload foto struk terlebih dahulu sebelum submit.</p>
<p id="err-foto" class="hidden text-xs text-red-500 mt-1 font-medium"></p>
</div>
<div class="flex flex-col items-center space-y-2">
<div class="bg-upst rounded-full p-3">
<i data-lucide="edit-3" class="w-7 h-7 text-white"></i>
</div>
<h2 class="text-xl font-bold text-gray-700">Data Struk</h2>
<p class="text-sm text-gray-500 text-center">Periksa dan lengkapi data struk sebelum submit.</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">Timbang</label>
<div id="timbang-options" class="grid grid-cols-2 gap-3">
<label class="timbang-opt flex items-center gap-2 p-3 border-2 rounded-lg cursor-pointer transition-all border-upst bg-upst/5" data-value="TPA">
<input type="radio" name="Timbang" value="TPA" checked class="hidden">
<div class="w-4 h-4 rounded-full border-2 border-upst flex items-center justify-center shrink-0">
<div class="w-2 h-2 rounded-full bg-upst timbang-dot"></div>
</div>
<span class="text-sm font-medium text-gray-700">Timbangan TPA</span>
</label>
<label class="timbang-opt flex items-center gap-2 p-3 border-2 rounded-lg cursor-pointer transition-all border-gray-200" data-value="RDF">
<input type="radio" name="Timbang" value="RDF" class="hidden">
<div class="w-4 h-4 rounded-full border-2 border-gray-300 flex items-center justify-center shrink-0">
<div class="w-2 h-2 rounded-full bg-transparent timbang-dot"></div>
</div>
<span class="text-sm font-medium text-gray-700">Timbangan RDF</span>
</label>
</div>
</div>
<div class="grid grid-cols-1 gap-4">
<div>
<label for="NomorStruk" class="block text-sm font-medium text-gray-700 mb-1">Nomor Struk <span class="text-red-500">*</span></label>
<input
type="text"
id="NomorStruk"
name="NomorStruk"
class="field-input mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition-all duration-150 px-4 py-2"
placeholder="8001441"
/>
<p id="err-NomorStruk" class="hidden text-xs text-red-500 mt-1"></p>
<p class="text-xs text-gray-500 mt-1">Nomor tanpa prefix (contoh: 8001441)</p>
</div>
<div>
<label for="NomorPolisi" class="block text-sm font-medium text-gray-700 mb-1">Nomor Polisi <span class="text-red-500">*</span></label>
<input
type="text"
id="NomorPolisi"
name="NomorPolisi"
class="field-input mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition-all duration-150 px-4 py-2"
placeholder="B 9125 PJA"
/>
<p id="err-NomorPolisi" class="hidden text-xs text-red-500 mt-1"></p>
</div>
<div>
<label for="Penugasan" class="block text-sm font-medium text-gray-700 mb-1">Penugasan <span class="text-red-500">*</span></label>
<input
type="text"
id="Penugasan"
name="Penugasan"
class="field-input mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition-all duration-150 px-4 py-2"
placeholder="JAKARTA BARAT"
/>
<p id="err-Penugasan" class="hidden text-xs text-red-500 mt-1"></p>
</div>
<div class="grid grid-cols-1 gap-3">
<div>
<label for="WaktuMasuk" class="block text-sm font-medium text-gray-700 mb-1">Masuk <span class="text-red-500">*</span></label>
<input
type="text"
id="WaktuMasuk"
name="WaktuMasuk"
class="field-input mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition-all duration-150 px-4 py-2"
placeholder="2025-08-04, 08:13:51"
/>
<p id="err-WaktuMasuk" class="hidden text-xs text-red-500 mt-1"></p>
</div>
<div>
<label for="WaktuKeluar" class="block text-sm font-medium text-gray-700 mb-1">Keluar <span class="text-red-500">*</span></label>
<input
type="text"
id="WaktuKeluar"
name="WaktuKeluar"
class="field-input mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition-all duration-150 px-4 py-2"
placeholder="2025-08-04, 14:35:10"
/>
<p id="err-WaktuKeluar" class="hidden text-xs text-red-500 mt-1"></p>
</div>
</div>
<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) <span class="text-red-500">*</span></label>
<input
type="number"
id="BeratMasuk"
name="BeratMasuk"
class="field-input mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition-all duration-150 px-4 py-2"
placeholder="23280"
/>
<p id="err-BeratMasuk" class="hidden text-xs text-red-500 mt-1"></p>
</div>
<div>
<label for="BeratKeluar" class="block text-sm font-medium text-gray-700 mb-1">Berat Keluar (kg) <span class="text-red-500">*</span></label>
<input
type="number"
id="BeratKeluar"
name="BeratKeluar"
class="field-input mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition-all duration-150 px-4 py-2"
placeholder="13540"
/>
<p id="err-BeratKeluar" class="hidden text-xs text-red-500 mt-1"></p>
</div>
</div>
<div>
<label for="BeratNett" class="block text-sm font-medium text-gray-700 mb-1">Berat Nett (kg) <span class="text-red-500">*</span></label>
<input
type="number"
id="BeratNett"
name="BeratNett"
class="field-input mt-1 block w-full rounded-lg border border-gray-300 shadow-sm focus:border-gray-500 focus:ring-2 focus:ring-gray-200 transition-all duration-150 px-4 py-2"
placeholder="9740"
/>
<p id="err-BeratNett" class="hidden text-xs text-red-500 mt-1"></p>
</div>
</div>
<button type="submit" class="w-full bg-upst text-white py-3 rounded-lg font-semibold shadow hover:from-gray-600 hover:to-gray-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/SpjDriverUpst/Shared/Components/_Navigation.cshtml" />
</div>
<register-block dynamic-section="scripts" key="jsSubmitStruk">
<script>
document.addEventListener('DOMContentLoaded', function() {
// Numeric-only inputs
['NomorStruk', 'BeratMasuk', 'BeratKeluar', 'BeratNett'].forEach(id => {
const el = document.getElementById(id);
if (el) el.addEventListener('input', function() { this.value = this.value.replace(/[^0-9]/g, ''); });
});
// Clear field error on input
['NomorStruk', 'NomorPolisi', 'Penugasan', 'WaktuMasuk', 'WaktuKeluar', 'BeratMasuk', 'BeratKeluar', 'BeratNett'].forEach(id => {
const el = document.getElementById(id);
el?.addEventListener('input', function() {
this.classList.remove('border-red-400', 'ring-2', 'ring-red-100');
const errEl = document.getElementById('err-' + id);
if (errEl) { errEl.classList.add('hidden'); errEl.textContent = ''; }
});
});
// Timbang radio visual styling
document.querySelectorAll('.timbang-opt').forEach(label => {
label.addEventListener('click', function() {
document.querySelectorAll('.timbang-opt').forEach(l => {
l.classList.remove('border-upst', 'bg-upst/5');
l.classList.add('border-gray-200');
const dot = l.querySelector('.timbang-dot');
if (dot) { dot.classList.remove('bg-upst'); dot.classList.add('bg-transparent'); }
const ring = l.querySelector('.rounded-full.border-2');
if (ring) { ring.classList.remove('border-upst'); ring.classList.add('border-gray-300'); }
});
this.classList.add('border-upst', 'bg-upst/5');
this.classList.remove('border-gray-200');
const dot = this.querySelector('.timbang-dot');
if (dot) { dot.classList.add('bg-upst'); dot.classList.remove('bg-transparent'); }
const ring = this.querySelector('.rounded-full.border-2');
if (ring) { ring.classList.add('border-upst'); ring.classList.remove('border-gray-300'); }
});
});
// ===== AI Receipt Scanner =====
class ReceiptScanner {
constructor() {
this.isScanning = false;
this.stream = null;
this.video = null;
this.canvas = null;
this.ctx = null;
this.capturedFile = null;
this.aiProcessing = false;
this.captureInterval = null;
this.startBtn = document.getElementById('start-scanner');
this.scannerActiveBtns = document.getElementById('scanner-active-btns');
this.stopBtn = document.getElementById('stop-scanner');
this.captureNowBtn = document.getElementById('capture-now');
this.loadingDiv = document.getElementById('loading-scanner');
this.aiProcessingDiv = document.getElementById('ocr-processing');
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');
this.bindEvents();
this.checkBrowserSupport();
}
bindEvents() {
this.startBtn?.addEventListener('click', () => this.startScanner());
this.stopBtn?.addEventListener('click', () => this.stopScanner());
this.captureNowBtn?.addEventListener('click', () => {
if (!this.aiProcessing) this.captureAndProcess();
});
this.fileUpload?.addEventListener('change', (e) => this.handleFileUpload(e));
}
checkBrowserSupport() {
if (!navigator.mediaDevices?.getUserMedia) {
if (this.startBtn) {
this.startBtn.disabled = true;
this.startBtn.textContent = 'Kamera tidak didukung di browser ini';
this.startBtn.classList.add('opacity-60', 'cursor-not-allowed');
}
}
}
async startScanner() {
try {
this.hideMessages();
this.permissionInfo?.classList.remove('hidden');
this.stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } }
});
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');
this.isScanning = true;
this.startBtn?.classList.add('hidden');
if (this.scannerActiveBtns) this.scannerActiveBtns.style.display = 'grid';
this.permissionInfo?.classList.add('hidden');
// Auto-capture: first after 2.5s, then every 6s
setTimeout(() => { if (this.isScanning && !this.aiProcessing) this.captureAndProcess(); }, 2500);
this.captureInterval = setInterval(() => {
if (this.isScanning && !this.aiProcessing) this.captureAndProcess();
}, 6000);
} catch (error) {
this.permissionInfo?.classList.add('hidden');
if (error.name === 'NotAllowedError') {
this.permissionDenied?.classList.remove('hidden');
} else if (error.name === 'NotFoundError') {
this.showError('Kamera tidak ditemukan pada perangkat ini.');
} else {
this.showError('Gagal mengakses kamera: ' + error.message);
}
}
}
async captureAndProcess() {
if (!this.video || !this.canvas || !this.isScanning || this.aiProcessing) return;
if (!this.video.videoWidth) return;
this.canvas.width = this.video.videoWidth;
this.canvas.height = this.video.videoHeight;
this.ctx.drawImage(this.video, 0, 0);
this.canvas.toBlob(async (blob) => {
if (blob) await this.processImageWithAI(blob);
}, 'image/jpeg', 0.85);
}
stopScanner() {
if (this.captureInterval) { clearInterval(this.captureInterval); this.captureInterval = null; }
if (this.stream) { this.stream.getTracks().forEach(t => t.stop()); this.stream = null; }
this.isScanning = false;
this.startBtn?.classList.remove('hidden');
if (this.scannerActiveBtns) this.scannerActiveBtns.style.display = 'none';
this.permissionInfo?.classList.add('hidden');
this.permissionDenied?.classList.add('hidden');
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">Kamera dihentikan</p>
</div>
</div>`;
this.loadingDiv = document.getElementById('loading-scanner');
}
async handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
this.showError('Harap pilih file gambar (JPG, PNG, dll.).');
event.target.value = ''; return;
}
if (file.size > 10 * 1024 * 1024) {
this.showError('Ukuran file terlalu besar. Maksimal 10MB.');
event.target.value = ''; return;
}
// Display image in scanner container
const reader = new FileReader();
reader.onload = (e) => {
const img = document.createElement('img');
img.src = e.target.result;
img.className = 'w-full h-full object-contain rounded-lg';
this.scannerContainer.innerHTML = '';
this.scannerContainer.appendChild(img);
};
reader.readAsDataURL(file);
if (this.isScanning) this.stopScanner();
this.hideMessages();
await this.processImageWithAI(file);
event.target.value = '';
}
async processImageWithAI(imageInput) {
if (this.aiProcessing) return;
this.aiProcessing = true;
this.aiProcessingDiv?.classList.remove('hidden');
try {
const formData = new FormData();
const fileName = imageInput instanceof File ? imageInput.name : 'struk.jpg';
formData.append('Foto', imageInput, fileName);
const response = await fetch('/upst/submit/ocr-struk', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success && result.data) {
this.applyAIData(result.data);
// Store file for form submission
this.capturedFile = imageInput instanceof File
? imageInput
: new File([imageInput], 'struk.jpg', { type: 'image/jpeg' });
this.updatePhotoInput();
this.showFotoPreview();
this.showScanSuccess();
this.vibrate();
if (this.isScanning) this.stopScanner();
setTimeout(() => {
document.getElementById('struk-form')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 600);
} else {
this.showError(result.message || 'AI tidak dapat membaca data struk. Coba upload foto yang lebih jelas.');
}
} catch (err) {
this.showError('Gagal menghubungi AI. Periksa koneksi dan coba lagi.');
} finally {
this.aiProcessing = false;
this.aiProcessingDiv?.classList.add('hidden');
}
}
applyAIData(data) {
const fields = [
{ id: 'NomorStruk', value: data.nomorStruk },
{ id: 'NomorPolisi', value: data.nomorPolisi },
{ id: 'Penugasan', value: data.penugasan },
{ id: 'WaktuMasuk', value: data.waktuMasuk },
{ id: 'WaktuKeluar', value: data.waktuKeluar },
{ id: 'BeratMasuk', value: data.beratMasuk },
{ id: 'BeratKeluar', value: data.beratKeluar },
{ id: 'BeratNett', value: data.beratNett },
];
fields.forEach(({ id, value }) => {
if (value !== null && value !== undefined && value !== '') {
const el = document.getElementById(id);
if (el) {
el.value = value;
el.classList.add('auto-filled');
el.dispatchEvent(new Event('input', { bubbles: true }));
setTimeout(() => el.classList.remove('auto-filled'), 3000);
}
}
});
}
updatePhotoInput() {
if (!this.capturedFile) return;
const input = document.getElementById('FotoStruk');
if (!input) return;
try {
const dt = new DataTransfer();
dt.items.add(this.capturedFile);
input.files = dt.files;
} catch (e) { /* fallback: manually appended on form submit */ }
}
showFotoPreview() {
if (!this.capturedFile) return;
const preview = document.getElementById('foto-struk-preview');
const img = document.getElementById('foto-struk-img');
const empty = document.getElementById('foto-struk-empty');
if (preview && img) {
img.src = URL.createObjectURL(this.capturedFile);
preview.classList.remove('hidden');
if (window.lucide) lucide.createIcons();
}
if (empty) empty.classList.add('hidden');
}
showScanSuccess() {
this.scanSuccess?.classList.remove('hidden');
setTimeout(() => this.scanSuccess?.classList.add('hidden'), 6000);
}
showError(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 gap-2"><i class="w-4 h-4 text-red-600 shrink-0" data-lucide="alert-circle"></i><span class="text-red-800 text-sm">${message}</span></div>`;
this.scannerContainer?.parentNode?.insertBefore(errorDiv, this.scannerContainer.nextSibling);
if (window.lucide) lucide.createIcons();
setTimeout(() => errorDiv.remove(), 6000);
}
hideMessages() {
this.permissionInfo?.classList.add('hidden');
this.permissionDenied?.classList.add('hidden');
this.aiProcessingDiv?.classList.add('hidden');
this.scanSuccess?.classList.add('hidden');
}
vibrate() {
if ('vibrate' in navigator) navigator.vibrate([200]);
}
}
const scanner = new ReceiptScanner();
// ===== Client-side validation =====
function clearErrors() {
document.querySelectorAll('.field-input').forEach(el => {
el.classList.remove('border-red-400', 'ring-2', 'ring-red-100');
});
document.querySelectorAll('[id^="err-"]').forEach(el => {
el.classList.add('hidden');
el.textContent = '';
});
}
function fieldError(id, message) {
const input = document.getElementById(id);
const errEl = document.getElementById('err-' + id);
if (input) {
input.classList.add('border-red-400', 'ring-2', 'ring-red-100');
input.classList.remove('border-gray-300');
}
if (errEl) {
errEl.textContent = message;
errEl.classList.remove('hidden');
}
}
function validateForm() {
clearErrors();
let firstErrorId = null;
const rules = [
{ id: 'NomorStruk', label: 'Nomor Struk', type: 'digits', minLen: 3 },
{ id: 'NomorPolisi', label: 'Nomor Polisi', type: 'text' },
{ id: 'Penugasan', label: 'Penugasan', type: 'text' },
{ id: 'WaktuMasuk', label: 'Waktu Masuk', type: 'text' },
{ id: 'WaktuKeluar', label: 'Waktu Keluar', type: 'text' },
{ id: 'BeratMasuk', label: 'Berat Masuk', type: 'number', min: 1 },
{ id: 'BeratKeluar', label: 'Berat Keluar', type: 'number', min: 1 },
{ id: 'BeratNett', label: 'Berat Nett', type: 'number', min: 100, max: 50000 },
];
rules.forEach(rule => {
const el = document.getElementById(rule.id);
if (!el) return;
const val = el.value.trim();
if (!val) {
fieldError(rule.id, `${rule.label} wajib diisi.`);
if (!firstErrorId) firstErrorId = rule.id;
return;
}
if (rule.type === 'digits' && !/^\d+$/.test(val)) {
fieldError(rule.id, `${rule.label} harus berupa angka.`);
if (!firstErrorId) firstErrorId = rule.id;
return;
}
if (rule.type === 'digits' && rule.minLen && val.length < rule.minLen) {
fieldError(rule.id, `${rule.label} minimal ${rule.minLen} digit.`);
if (!firstErrorId) firstErrorId = rule.id;
return;
}
if (rule.type === 'number') {
const num = Number(val);
if (isNaN(num) || num <= 0) {
fieldError(rule.id, `${rule.label} harus berupa angka positif.`);
if (!firstErrorId) firstErrorId = rule.id;
return;
}
if (rule.min !== undefined && num < rule.min) {
fieldError(rule.id, `${rule.label} minimal ${rule.min} kg.`);
if (!firstErrorId) firstErrorId = rule.id;
return;
}
if (rule.max !== undefined && num > rule.max) {
fieldError(rule.id, `${rule.label} maksimal ${rule.max.toLocaleString('id-ID')} kg.`);
if (!firstErrorId) firstErrorId = rule.id;
}
}
});
// Foto struk wajib
const fotoInput = document.getElementById('FotoStruk');
const hasFoto = scanner.capturedFile || fotoInput?.files?.length;
if (!hasFoto) {
const errEl = document.getElementById('err-foto');
if (errEl) { errEl.textContent = 'Foto struk wajib diupload atau di-scan.'; errEl.classList.remove('hidden'); }
if (!firstErrorId) firstErrorId = 'foto-struk-preview';
}
if (firstErrorId) {
const scrollTarget = document.getElementById(firstErrorId) || document.getElementById('foto-struk-preview');
scrollTarget?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return false;
}
return true;
}
// Form submit: validate then submit
document.getElementById('struk-form')?.addEventListener('submit', async function(e) {
e.preventDefault();
if (!validateForm()) return;
const formData = new FormData(this);
// Fallback: manually append if DataTransfer API did not set the file input
const fotoInput = document.getElementById('FotoStruk');
if (scanner.capturedFile && (!fotoInput?.files?.length)) {
formData.append('FotoStruk', scanner.capturedFile, 'struk.jpg');
}
const submitBtn = this.querySelector('[type="submit"]');
const originalHtml = submitBtn?.innerHTML;
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<div class="loading-spinner-small inline-block mr-2"></div>Menyimpan...';
}
try {
const response = await fetch(this.action, { method: 'POST', body: formData });
if (response.redirected) {
window.location.href = response.url;
} else {
window.location.reload();
}
} catch (err) {
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.innerHTML = originalHtml;
if (window.lucide) lucide.createIcons();
}
alert('Gagal submit. Silakan coba lagi.');
}
});
});
</script>
</register-block>