← back to Watches

public/camera-scanner.js

367 lines

/**
 * Camera Scanner for Watch Reference Number Recognition
 * Supports barcode scanning, OCR for reference numbers, and image capture
 */

class WatchScanner {
  constructor() {
    this.stream = null;
    this.videoElement = null;
    this.canvasElement = null;
    this.isScanning = false;
    this.onScanCallback = null;
    this.constraints = {
      video: {
        facingMode: 'environment',
        width: { ideal: 1920 },
        height: { ideal: 1080 },
        focusMode: 'continuous'
      }
    };
  }

  /**
   * Initialize camera scanner
   */
  async init(videoElementId, canvasElementId) {
    this.videoElement = document.getElementById(videoElementId);
    this.canvasElement = document.getElementById(canvasElementId);

    if (!this.videoElement || !this.canvasElement) {
      throw new Error('Video or canvas element not found');
    }

    // Check if camera is available
    if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
      throw new Error('Camera not supported on this device');
    }

    return this;
  }

  /**
   * Start camera stream
   */
  async start() {
    try {
      this.stream = await navigator.mediaDevices.getUserMedia(this.constraints);
      this.videoElement.srcObject = this.stream;
      this.videoElement.play();

      console.log('[Scanner] Camera started');

      // Wait for video to be ready
      return new Promise((resolve) => {
        this.videoElement.onloadedmetadata = () => {
          resolve(this);
        };
      });
    } catch (error) {
      console.error('[Scanner] Failed to start camera:', error);
      throw new Error('Unable to access camera: ' + error.message);
    }
  }

  /**
   * Stop camera stream
   */
  stop() {
    if (this.stream) {
      this.stream.getTracks().forEach(track => track.stop());
      this.stream = null;
      this.isScanning = false;
      console.log('[Scanner] Camera stopped');
    }
  }

  /**
   * Capture current frame
   */
  captureFrame() {
    const context = this.canvasElement.getContext('2d');
    this.canvasElement.width = this.videoElement.videoWidth;
    this.canvasElement.height = this.videoElement.videoHeight;

    context.drawImage(
      this.videoElement,
      0, 0,
      this.canvasElement.width,
      this.canvasElement.height
    );

    return this.canvasElement.toDataURL('image/jpeg', 0.8);
  }

  /**
   * Start continuous scanning for watch reference numbers
   */
  startScanning(callback) {
    this.isScanning = true;
    this.onScanCallback = callback;

    const scan = () => {
      if (!this.isScanning) return;

      try {
        const imageData = this.captureFrame();
        this.detectWatchReference(imageData);
      } catch (error) {
        console.error('[Scanner] Scan error:', error);
      }

      // Scan every 500ms
      setTimeout(scan, 500);
    };

    scan();
    console.log('[Scanner] Scanning started');
  }

  /**
   * Stop scanning
   */
  stopScanning() {
    this.isScanning = false;
    console.log('[Scanner] Scanning stopped');
  }

  /**
   * Detect watch reference number from image
   * This is a simplified version - in production, you'd use:
   * - Tesseract.js for OCR
   * - ZXing for barcode scanning
   * - Cloud Vision API for better accuracy
   */
  async detectWatchReference(imageData) {
    try {
      // Simulate OCR detection (in production, integrate Tesseract.js)
      const mockResult = this.simulateOCR(imageData);

      if (mockResult.found && this.onScanCallback) {
        this.onScanCallback({
          success: true,
          referenceNumber: mockResult.reference,
          confidence: mockResult.confidence,
          imageData: imageData
        });
      }
    } catch (error) {
      console.error('[Scanner] Detection error:', error);
    }
  }

  /**
   * Simulate OCR (replace with real OCR in production)
   */
  simulateOCR(imageData) {
    // In production, use Tesseract.js or similar
    // For demo purposes, we'll return mock data
    return {
      found: false,
      reference: null,
      confidence: 0
    };
  }

  /**
   * Search for watch by reference number
   */
  async searchByReference(referenceNumber) {
    try {
      const response = await fetch(`/api/search?q=${encodeURIComponent(referenceNumber)}`);
      const data = await response.json();

      if (data.results && data.results.length > 0) {
        return {
          success: true,
          watches: data.results,
          message: `Found ${data.results.length} matching watch(es)`
        };
      } else {
        return {
          success: false,
          message: 'No watches found with this reference number'
        };
      }
    } catch (error) {
      console.error('[Scanner] Search error:', error);
      return {
        success: false,
        error: error.message
      };
    }
  }

  /**
   * Save scanned watch to IndexedDB for offline access
   */
  async saveScan(scanData) {
    try {
      const db = await this.openDB();
      const tx = db.transaction('scannedWatches', 'readwrite');
      const store = tx.objectStore('scannedWatches');

      const scan = {
        timestamp: Date.now(),
        referenceNumber: scanData.referenceNumber,
        imageData: scanData.imageData,
        confidence: scanData.confidence,
        synced: false,
        data: scanData
      };

      await new Promise((resolve, reject) => {
        const request = store.add(scan);
        request.onsuccess = () => resolve(request.result);
        request.onerror = () => reject(request.error);
      });

      console.log('[Scanner] Scan saved to IndexedDB');

      // Register background sync
      if ('serviceWorker' in navigator && 'SyncManager' in window) {
        const registration = await navigator.serviceWorker.ready;
        await registration.sync.register('sync-scanned-watches');
      }

      return true;
    } catch (error) {
      console.error('[Scanner] Failed to save scan:', error);
      return false;
    }
  }

  /**
   * Get scan history
   */
  async getScanHistory() {
    try {
      const db = await this.openDB();
      const tx = db.transaction('scannedWatches', 'readonly');
      const store = tx.objectStore('scannedWatches');

      return new Promise((resolve, reject) => {
        const request = store.getAll();
        request.onsuccess = () => resolve(request.result.reverse());
        request.onerror = () => reject(request.error);
      });
    } catch (error) {
      console.error('[Scanner] Failed to get history:', error);
      return [];
    }
  }

  /**
   * Switch camera (front/back)
   */
  async switchCamera() {
    const currentMode = this.constraints.video.facingMode;
    this.constraints.video.facingMode = currentMode === 'environment' ? 'user' : 'environment';

    this.stop();
    await this.start();

    console.log('[Scanner] Camera switched to:', this.constraints.video.facingMode);
  }

  /**
   * Enable torch/flashlight (if available)
   */
  async toggleTorch() {
    try {
      const track = this.stream.getVideoTracks()[0];
      const capabilities = track.getCapabilities();

      if (capabilities.torch) {
        const settings = track.getSettings();
        await track.applyConstraints({
          advanced: [{ torch: !settings.torch }]
        });

        console.log('[Scanner] Torch toggled');
        return true;
      } else {
        console.warn('[Scanner] Torch not supported');
        return false;
      }
    } catch (error) {
      console.error('[Scanner] Torch error:', error);
      return false;
    }
  }

  /**
   * Take photo with camera
   */
  async takePhoto() {
    const imageData = this.captureFrame();

    // Save to IndexedDB
    const db = await this.openDB();
    const tx = db.transaction('scannedWatches', 'readwrite');
    const store = tx.objectStore('scannedWatches');

    const photo = {
      timestamp: Date.now(),
      imageData: imageData,
      type: 'manual_capture'
    };

    return new Promise((resolve, reject) => {
      const request = store.add(photo);
      request.onsuccess = () => resolve(imageData);
      request.onerror = () => reject(request.error);
    });
  }

  /**
   * Open IndexedDB
   */
  openDB() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open('OmegaWatchesPWA', 2);

      request.onerror = () => reject(request.error);
      request.onsuccess = () => resolve(request.result);

      request.onupgradeneeded = (event) => {
        const db = event.target.result;

        if (!db.objectStoreNames.contains('scannedWatches')) {
          const store = db.createObjectStore('scannedWatches', {
            keyPath: 'id',
            autoIncrement: true
          });
          store.createIndex('timestamp', 'timestamp', { unique: false });
          store.createIndex('synced', 'synced', { unique: false });
        }
      };
    });
  }

  /**
   * Get camera capabilities
   */
  getCapabilities() {
    if (!this.stream) return null;

    const track = this.stream.getVideoTracks()[0];
    return track.getCapabilities();
  }

  /**
   * Get current settings
   */
  getSettings() {
    if (!this.stream) return null;

    const track = this.stream.getVideoTracks()[0];
    return track.getSettings();
  }
}

// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
  module.exports = WatchScanner;
}