import React, { useState, useRef, useEffect, useCallback } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import {
ScanLine, Camera, Keyboard, CheckCircle2, XCircle,
Eye, Pencil, ShoppingCart, RotateCcw, Zap
} from 'lucide-react'
import { useData } from '../context/DataContext'
import StatusBadge from '../components/StatusBadge'
import CategoryBadge from '../components/CategoryBadge'
// ─── Bluetooth / text-input scanner mode ─────────────────────────────────────
function BluetoothScanner({ onScan }) {
const [value, setValue] = useState('')
const inputRef = useRef(null)
useEffect(() => { inputRef.current?.focus() }, [])
function handleKeyDown(e) {
if (e.key === 'Enter') {
const trimmed = value.trim()
if (trimmed) { onScan(trimmed); setValue('') }
}
}
return (
Bluetooth Scanner Mode
Connect your Bluetooth barcode scanner, click the field, and scan any item label.
The scanner will auto-submit when it sends Enter.
setValue(e.target.value)}
onKeyDown={handleKeyDown}
autoFocus
autoComplete="off"
spellCheck={false}
/>
Or type an asset tag (e.g. TR-A1B2C3) and press Enter
)
}
// ─── Camera scanner mode ──────────────────────────────────────────────────────
function CameraScanner({ onScan }) {
const videoRef = useRef(null)
const canvasRef = useRef(null)
const frameRef = useRef(null)
const [status, setStatus] = useState('idle') // idle | starting | running | error
const [errorMsg, setErrorMsg] = useState('')
const [lastScan, setLastScan] = useState(null)
async function startCamera() {
setStatus('starting')
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } })
videoRef.current.srcObject = stream
await videoRef.current.play()
setStatus('running')
scanLoop()
} catch (err) {
setStatus('error')
setErrorMsg(err.message || 'Camera access denied')
}
}
function stopCamera() {
cancelAnimationFrame(frameRef.current)
const stream = videoRef.current?.srcObject
stream?.getTracks().forEach(t => t.stop())
if (videoRef.current) videoRef.current.srcObject = null
setStatus('idle')
}
useEffect(() => () => stopCamera(), [])
async function scanLoop() {
if (!videoRef.current || !canvasRef.current) return
const video = videoRef.current
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
async function tick() {
if (video.readyState === video.HAVE_ENOUGH_DATA) {
canvas.width = video.videoWidth
canvas.height = video.videoHeight
ctx.drawImage(video, 0, 0)
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height)
try {
// Dynamic import jsQR
const jsQR = (await import('jsqr')).default
const code = jsQR(imgData.data, imgData.width, imgData.height)
if (code && code.data !== lastScan) {
setLastScan(code.data)
onScan(code.data)
}
} catch (_) {}
}
frameRef.current = requestAnimationFrame(tick)
}
frameRef.current = requestAnimationFrame(tick)
}
return (
Camera Scanner Mode
Uses your device camera to scan QR codes from printed labels.
Requires camera permission.
{status === 'idle' && (
Camera is not active.
)}
{status === 'starting' && (
)}
{status === 'running' && (
)}
{status === 'error' && (
Camera Error
{errorMsg}
)}
)
}
// ─── Scan result card ─────────────────────────────────────────────────────────
function ScanResult({ item, onMarkSold, onClear, settings }) {
const sym = settings.currencySymbol
const fmt = n => n != null ? `${sym}${Number(n).toFixed(2)}` : '—'
if (!item) return null
return (
Item Found
{item.assetTag}
{item.name}
{item.condition}
List Price
{fmt(item.listPrice)}
Cost: {fmt(item.purchasePrice)}
{item.description && (
{item.description}
)}
{/* Quick actions */}
View Full Item
Edit
{item.status !== 'Sold' && (
)}
)
}
// ─── Scan not found ───────────────────────────────────────────────────────────
function ScanNotFound({ query, onClear }) {
return (
No item found for:
{query}
Check the asset tag and try again.
)
}
// ─── Main page ────────────────────────────────────────────────────────────────
export default function Scanner() {
const { getItemByTag, updateItem, settings } = useData()
const [mode, setMode] = useState('bluetooth') // bluetooth | camera
const [result, setResult] = useState(null) // { item, query } | null
const [soldMsg, setSoldMsg] = useState(false)
const handleScan = useCallback((raw) => {
// Raw might be full QR value like "Texas Resellers|TR-A1B2C3|Name"
// or just the asset tag like "TR-A1B2C3"
let query = raw.trim()
if (query.includes('|')) {
const parts = query.split('|')
query = parts[1] || query
}
const item = getItemByTag(query)
setResult({ item: item || null, query })
setSoldMsg(false)
}, [getItemByTag])
function handleMarkSold() {
if (!result?.item) return
updateItem(result.item.id, {
status: 'Sold',
dateSold: new Date().toISOString().split('T')[0],
})
setSoldMsg(true)
// Refresh result with updated item
setTimeout(() => {
setResult(prev => ({
...prev,
item: { ...prev.item, status: 'Sold', dateSold: new Date().toISOString().split('T')[0] }
}))
}, 100)
}
function handleClear() {
setResult(null)
setSoldMsg(false)
}
return (
{/* Header */}
Scanner
Scan a barcode or QR code from a printed label.
{/* Mode tabs */}
{/* Scanner UI */}
{mode === 'bluetooth' &&
}
{mode === 'camera' &&
}
{/* Result */}
{result && (
{soldMsg && (
Item marked as sold!
)}
{result.item
?
:
}
)}
{/* Recent scans hint */}
{!result && (
Scan result will appear here
)}
)
}