1. Auto dark mode: detect system prefers-color-scheme, auto-switch industrial/modern theme 2. Email quotation endpoint: POST /quotations/:id/email sends HTML email via SMTP 3. Camera barcode scanner: BarcodeDetector API with getUserMedia overlay in catalog 4. Returns with warranty: POST /returns endpoint with stock restoration and sale status tracking 5. Partial offline catalog: cache top 500 parts in IndexedDB, search when offline Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
6.8 KiB
JavaScript
165 lines
6.8 KiB
JavaScript
// native-bridge.js — Detects if running inside Capacitor native app
|
|
// and provides native API access (camera for barcode, push notifications, haptics)
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
window.NexusNative = {
|
|
isNative: typeof Capacitor !== 'undefined',
|
|
_scanStream: null,
|
|
_scanVideo: null,
|
|
|
|
// Camera barcode scanning — works in native (Capacitor) and web (BarcodeDetector / getUserMedia)
|
|
async scanBarcode() {
|
|
// Native Capacitor path
|
|
if (this.isNative) {
|
|
try {
|
|
const { Camera } = await import('@capacitor/camera');
|
|
const photo = await Camera.getPhoto({
|
|
quality: 90,
|
|
resultType: 'base64'
|
|
});
|
|
return photo;
|
|
} catch(e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Web path: use BarcodeDetector API (Chrome 83+)
|
|
if (!('BarcodeDetector' in window)) {
|
|
alert('Tu navegador no soporta escaneo de codigos de barras. Usa Chrome 83+ o un dispositivo movil.');
|
|
return null;
|
|
}
|
|
|
|
return new Promise(async (resolve) => {
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: { facingMode: 'environment', width: { ideal: 1280 }, height: { ideal: 720 } }
|
|
});
|
|
this._scanStream = stream;
|
|
|
|
// Create overlay UI
|
|
const overlay = document.createElement('div');
|
|
overlay.id = 'barcode-scan-overlay';
|
|
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;';
|
|
|
|
const video = document.createElement('video');
|
|
video.autoplay = true;
|
|
video.playsInline = true;
|
|
video.style.cssText = 'width:90%;max-width:500px;border-radius:12px;border:3px solid #F5A623;';
|
|
video.srcObject = stream;
|
|
this._scanVideo = video;
|
|
|
|
const label = document.createElement('p');
|
|
label.textContent = 'Apunta al codigo de barras...';
|
|
label.style.cssText = 'color:#fff;font-size:16px;margin-top:16px;font-family:sans-serif;';
|
|
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.textContent = 'Cancelar';
|
|
cancelBtn.style.cssText = 'margin-top:16px;padding:10px 24px;background:#F5A623;color:#000;border:none;border-radius:6px;font-size:15px;cursor:pointer;font-weight:bold;';
|
|
cancelBtn.onclick = () => {
|
|
this.stopScan();
|
|
overlay.remove();
|
|
resolve(null);
|
|
};
|
|
|
|
overlay.appendChild(video);
|
|
overlay.appendChild(label);
|
|
overlay.appendChild(cancelBtn);
|
|
document.body.appendChild(overlay);
|
|
|
|
const detector = new BarcodeDetector({
|
|
formats: ['ean_13', 'ean_8', 'code_128', 'code_39', 'qr_code', 'upc_a', 'upc_e']
|
|
});
|
|
|
|
const scanFrame = async () => {
|
|
if (!this._scanStream) return;
|
|
try {
|
|
const barcodes = await detector.detect(video);
|
|
if (barcodes.length > 0) {
|
|
const code = barcodes[0].rawValue;
|
|
label.textContent = 'Codigo detectado: ' + code;
|
|
label.style.color = '#4CAF50';
|
|
// Small delay so user sees the result
|
|
setTimeout(() => {
|
|
this.stopScan();
|
|
overlay.remove();
|
|
resolve(code);
|
|
}, 400);
|
|
return;
|
|
}
|
|
} catch(e) { /* frame failed, retry */ }
|
|
requestAnimationFrame(scanFrame);
|
|
};
|
|
|
|
// Wait for video to be ready
|
|
video.onloadedmetadata = () => {
|
|
video.play();
|
|
requestAnimationFrame(scanFrame);
|
|
};
|
|
|
|
} catch(e) {
|
|
console.error('Camera access error:', e);
|
|
alert('No se pudo acceder a la camara: ' + e.message);
|
|
resolve(null);
|
|
}
|
|
});
|
|
},
|
|
|
|
stopScan() {
|
|
if (this._scanStream) {
|
|
this._scanStream.getTracks().forEach(t => t.stop());
|
|
this._scanStream = null;
|
|
}
|
|
this._scanVideo = null;
|
|
var overlay = document.getElementById('barcode-scan-overlay');
|
|
if (overlay) overlay.remove();
|
|
},
|
|
|
|
// Push notification registration
|
|
async registerPush() {
|
|
if (!this.isNative) return null;
|
|
try {
|
|
const { PushNotifications } = await import('@capacitor/push-notifications');
|
|
const result = await PushNotifications.requestPermissions();
|
|
if (result.receive === 'granted') {
|
|
await PushNotifications.register();
|
|
}
|
|
PushNotifications.addListener('registration', token => {
|
|
console.log('Push token:', token.value);
|
|
});
|
|
PushNotifications.addListener('pushNotificationReceived', notification => {
|
|
console.log('Push received:', notification);
|
|
});
|
|
} catch(e) {
|
|
console.log('Push not available:', e);
|
|
}
|
|
},
|
|
|
|
// Haptic feedback
|
|
async vibrate() {
|
|
if (!this.isNative) return;
|
|
try {
|
|
const { Haptics, ImpactStyle } = await import('@capacitor/haptics');
|
|
await Haptics.impact({ style: ImpactStyle.Light });
|
|
} catch(e) {}
|
|
},
|
|
|
|
// Status bar (hide for fullscreen POS)
|
|
async setupStatusBar() {
|
|
if (!this.isNative) return;
|
|
try {
|
|
const { StatusBar, Style } = await import('@capacitor/status-bar');
|
|
await StatusBar.setStyle({ style: Style.Dark });
|
|
await StatusBar.setBackgroundColor({ color: '#0d0d0d' });
|
|
} catch(e) {}
|
|
}
|
|
};
|
|
|
|
// Auto-init if native
|
|
if (window.NexusNative.isNative) {
|
|
window.NexusNative.setupStatusBar();
|
|
window.NexusNative.registerPush();
|
|
}
|
|
})();
|