598 lines
27 KiB
Plaintext
598 lines
27 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">
|
|
<!-- Header with Orange Background -->
|
|
<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">
|
|
<!-- Camera Scanner Section -->
|
|
<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 untuk membaca data secara otomatis.</p>
|
|
</div>
|
|
|
|
<!-- Scanner Container -->
|
|
<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>
|
|
|
|
<!-- Scanner Controls -->
|
|
<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>
|
|
|
|
<!-- Permission Messages -->
|
|
<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>
|
|
|
|
<!-- OCR Processing Message -->
|
|
<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>
|
|
|
|
<!-- OCR Result -->
|
|
<div id="ocr-result" class="hidden bg-green-50 border border-green-200 rounded-lg p-3">
|
|
<div class="flex items-center mb-2">
|
|
<i class="w-5 h-5 text-green-600 mr-2" data-lucide="check-circle"></i>
|
|
<span class="text-green-800 font-medium">Data struk berhasil dibaca!</span>
|
|
</div>
|
|
<div class="text-green-700 text-sm space-y-1">
|
|
<p>Nomor Struk: <span id="detected-receipt-number" class="font-mono font-bold">-</span></p>
|
|
<p>Berat Muatan: <span id="detected-weight" class="font-mono font-bold">-</span> kg</p>
|
|
</div>
|
|
<div class="flex gap-2 mt-3">
|
|
<button id="apply-ocr-data" type="button" class="bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded-lg transition-colors text-sm">
|
|
Gunakan Data Ini
|
|
</button>
|
|
<button id="retry-ocr" type="button" class="bg-gray-500 hover:bg-gray-600 text-white px-3 py-2 rounded-lg transition-colors text-sm">
|
|
Scan Ulang
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tips for scanning -->
|
|
<div class="bg-gray-50 border border-gray-200 rounded-lg p-3 text-sm">
|
|
<div class="flex items-start">
|
|
<i class="w-4 h-4 text-gray-600 mr-2 mt-0.5" data-lucide="lightbulb"></i>
|
|
<div class="text-gray-700">
|
|
<p class="font-medium mb-1">Tips Scan Struk:</p>
|
|
<ul class="text-xs space-y-1">
|
|
<li>• Pastikan struk dalam pencahayaan yang cukup</li>
|
|
<li>• Letakkan struk rata tanpa lipatan</li>
|
|
<li>• Jaga jarak 15-30cm dari kamera</li>
|
|
<li>• Tunggu beberapa detik untuk proses OCR</li>
|
|
<li>• Periksa data yang terdeteksi sebelum submit</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Divider -->
|
|
<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">atau input manual</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">Input Manual</h2>
|
|
<p class="text-sm text-gray-500 text-center">Masukkan nomor struk dan berat muatan secara manual.</p>
|
|
</div>
|
|
|
|
<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"
|
|
inputmode="numeric"
|
|
pattern="[0-9]*"
|
|
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="1234567890"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="BeratMuatan" class="block text-sm font-medium text-gray-700 mb-1">Berat Muatan (kg)</label>
|
|
<input
|
|
type="text"
|
|
id="BeratMuatan"
|
|
name="BeratMuatan"
|
|
inputmode="decimal"
|
|
pattern="[0-9]*\.?[0-9]*"
|
|
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="1500"
|
|
/>
|
|
</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
|
|
</button>
|
|
</form>
|
|
|
|
<!-- Bottom Navigation -->
|
|
<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 beratMuatanInput = document.getElementById('BeratMuatan');
|
|
|
|
// Input validation for manual entry
|
|
nomorStrukInput.addEventListener('input', function() {
|
|
this.value = this.value.replace(/[^0-9]/g, '');
|
|
});
|
|
|
|
beratMuatanInput.addEventListener('input', function() {
|
|
this.value = this.value.replace(/[^0-9.]/g, '');
|
|
});
|
|
|
|
// Initialize Receipt Scanner with OCR
|
|
class ReceiptScanner {
|
|
constructor() {
|
|
this.isScanning = false;
|
|
this.stream = null;
|
|
this.video = null;
|
|
this.canvas = null;
|
|
this.ctx = null;
|
|
this.detectedData = {
|
|
receiptNumber: '',
|
|
weight: ''
|
|
};
|
|
this.initializeElements();
|
|
this.bindEvents();
|
|
this.checkBrowserSupport();
|
|
}
|
|
|
|
initializeElements() {
|
|
this.startBtn = document.getElementById('start-scanner');
|
|
this.stopBtn = document.getElementById('stop-scanner');
|
|
this.loadingDiv = document.getElementById('loading-scanner');
|
|
this.ocrProcessing = document.getElementById('ocr-processing');
|
|
this.ocrResult = document.getElementById('ocr-result');
|
|
this.permissionInfo = document.getElementById('permission-info');
|
|
this.permissionDenied = document.getElementById('permission-denied');
|
|
this.applyDataBtn = document.getElementById('apply-ocr-data');
|
|
this.retryOcrBtn = document.getElementById('retry-ocr');
|
|
this.detectedReceiptSpan = document.getElementById('detected-receipt-number');
|
|
this.detectedWeightSpan = document.getElementById('detected-weight');
|
|
this.scannerContainer = document.getElementById('scanner-container');
|
|
}
|
|
|
|
bindEvents() {
|
|
this.startBtn.addEventListener('click', () => this.startScanner());
|
|
this.stopBtn.addEventListener('click', () => this.stopScanner());
|
|
this.applyDataBtn.addEventListener('click', () => this.applyDetectedData());
|
|
this.retryOcrBtn.addEventListener('click', () => this.retryOcr());
|
|
}
|
|
|
|
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');
|
|
|
|
// Get camera access
|
|
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();
|
|
|
|
// Start continuous capture for OCR
|
|
this.startContinuousCapture();
|
|
|
|
} catch (error) {
|
|
this.handleCameraError(error);
|
|
this.hideLoading();
|
|
}
|
|
}
|
|
|
|
setupVideoElement() {
|
|
// Create video element
|
|
this.video = document.createElement('video');
|
|
this.video.autoplay = true;
|
|
this.video.playsInline = true;
|
|
this.video.muted = true;
|
|
this.video.srcObject = this.stream;
|
|
|
|
// Style video element
|
|
this.video.className = 'w-full h-full object-cover rounded-lg';
|
|
|
|
// Clear container and add video
|
|
this.scannerContainer.innerHTML = '';
|
|
this.scannerContainer.appendChild(this.video);
|
|
|
|
// Create canvas for capture
|
|
this.canvas = document.createElement('canvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
}
|
|
|
|
startContinuousCapture() {
|
|
// Capture frame every 3 seconds for OCR processing
|
|
const captureInterval = setInterval(() => {
|
|
if (!this.isScanning) {
|
|
clearInterval(captureInterval);
|
|
return;
|
|
}
|
|
this.captureAndProcessFrame();
|
|
}, 3000);
|
|
}
|
|
|
|
async captureAndProcessFrame() {
|
|
if (!this.video || !this.canvas || !this.isScanning) return;
|
|
|
|
try {
|
|
// Set canvas size to video size
|
|
this.canvas.width = this.video.videoWidth;
|
|
this.canvas.height = this.video.videoHeight;
|
|
|
|
// Draw video frame to canvas
|
|
this.ctx.drawImage(this.video, 0, 0);
|
|
|
|
// Convert to blob for OCR
|
|
this.canvas.toBlob(async (blob) => {
|
|
await this.processImageWithOCR(blob);
|
|
}, 'image/jpeg', 0.8);
|
|
|
|
} catch (error) {
|
|
console.error('Error capturing frame:', error);
|
|
}
|
|
}
|
|
|
|
async processImageWithOCR(imageBlob) {
|
|
try {
|
|
this.showOcrProcessing();
|
|
|
|
const { data: { text } } = await Tesseract.recognize(
|
|
imageBlob,
|
|
'ind+eng',
|
|
{
|
|
logger: m => {
|
|
// Optional: show OCR progress
|
|
if (m.status === 'recognizing text') {
|
|
console.log(`OCR Progress: ${Math.round(m.progress * 100)}%`);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
await this.extractDataFromText(text);
|
|
this.hideOcrProcessing();
|
|
|
|
} catch (error) {
|
|
console.error('OCR Error:', error);
|
|
this.hideOcrProcessing();
|
|
}
|
|
}
|
|
|
|
async extractDataFromText(text) {
|
|
const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
|
|
let receiptNumber = '';
|
|
let weight = '';
|
|
|
|
// Patterns for receipt number detection
|
|
const receiptPatterns = [
|
|
/(?:no|nomor|receipt|struk|bon)[\s.:]*([0-9]{6,})/i,
|
|
/([0-9]{8,})/g, // Long number sequences
|
|
/(?:ticket|tiket)[\s.:]*([0-9]+)/i
|
|
];
|
|
|
|
// Patterns for weight detection
|
|
const weightPatterns = [
|
|
/(?:berat|weight|kg|kilogram)[\s.:]*([0-9]+(?:\.[0-9]+)?)/i,
|
|
/([0-9]+(?:\.[0-9]+)?)[\s]*(?:kg|kilogram)/i,
|
|
/(?:muatan|load)[\s.:]*([0-9]+(?:\.[0-9]+)?)/i
|
|
];
|
|
|
|
// Search for receipt number
|
|
for (const line of lines) {
|
|
for (const pattern of receiptPatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1] && match[1].length >= 6) {
|
|
receiptNumber = match[1];
|
|
break;
|
|
}
|
|
}
|
|
if (receiptNumber) break;
|
|
}
|
|
|
|
// Search for weight
|
|
for (const line of lines) {
|
|
for (const pattern of weightPatterns) {
|
|
const match = line.match(pattern);
|
|
if (match && match[1]) {
|
|
const weightValue = parseFloat(match[1]);
|
|
if (weightValue > 0 && weightValue < 100000) { // Reasonable weight range
|
|
weight = match[1];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (weight) break;
|
|
}
|
|
|
|
// If we found either piece of data, show results
|
|
if (receiptNumber || weight) {
|
|
this.detectedData.receiptNumber = receiptNumber;
|
|
this.detectedData.weight = weight;
|
|
this.showOcrResult();
|
|
this.playSuccessSound();
|
|
this.vibrate();
|
|
|
|
// Auto-stop scanning after successful detection
|
|
setTimeout(() => {
|
|
this.stopScanner();
|
|
}, 1000);
|
|
}
|
|
}
|
|
|
|
showOcrResult() {
|
|
this.detectedReceiptSpan.textContent = this.detectedData.receiptNumber || 'Tidak terdeteksi';
|
|
this.detectedWeightSpan.textContent = this.detectedData.weight || 'Tidak terdeteksi';
|
|
this.ocrResult.classList.remove('hidden');
|
|
}
|
|
|
|
applyDetectedData() {
|
|
if (this.detectedData.receiptNumber) {
|
|
nomorStrukInput.value = this.detectedData.receiptNumber;
|
|
nomorStrukInput.classList.add('auto-filled');
|
|
setTimeout(() => nomorStrukInput.classList.remove('auto-filled'), 2000);
|
|
}
|
|
if (this.detectedData.weight) {
|
|
beratMuatanInput.value = this.detectedData.weight;
|
|
beratMuatanInput.classList.add('auto-filled');
|
|
setTimeout(() => beratMuatanInput.classList.remove('auto-filled'), 2000);
|
|
}
|
|
this.hideMessages();
|
|
|
|
// Show success message
|
|
this.showSuccess('Data dari scan telah diisi ke form!');
|
|
}
|
|
|
|
async retryOcr() {
|
|
this.hideMessages();
|
|
this.detectedData = { receiptNumber: '', weight: '' };
|
|
|
|
if (this.isScanning) {
|
|
await this.stopScanner();
|
|
}
|
|
|
|
setTimeout(() => {
|
|
this.startScanner();
|
|
}, 500);
|
|
}
|
|
|
|
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();
|
|
this.hideMessages();
|
|
|
|
// 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.ocrResult.classList.add('hidden');
|
|
}
|
|
|
|
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
|
|
new ReceiptScanner();
|
|
});
|
|
</script>
|
|
</register-block> |