diff --git a/src/components/BarcodeScannerView.tsx b/src/components/BarcodeScannerView.tsx index 381edb7..a7cdcc7 100644 --- a/src/components/BarcodeScannerView.tsx +++ b/src/components/BarcodeScannerView.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { Alert, Card, CardContent, Typography } from '@mui/material'; import { useBarcodeScanner } from '../hooks/useBarcodeScanner'; @@ -8,9 +9,11 @@ interface BarcodeScannerViewProps { export const BarcodeScannerView = ({ onDetected }: BarcodeScannerViewProps) => { const { videoRef, result } = useBarcodeScanner(); - if (result.code && onDetected) { - onDetected(result.code); - } + useEffect(() => { + if (result.code && onDetected) { + onDetected(result.code); + } + }, [onDetected, result.code]); return ( diff --git a/src/components/ProductRow.tsx b/src/components/ProductRow.tsx index beac226..c93d2c7 100644 --- a/src/components/ProductRow.tsx +++ b/src/components/ProductRow.tsx @@ -2,18 +2,23 @@ import { Card, CardActions, CardContent, + Dialog, + DialogContent, + DialogTitle, IconButton, MenuItem, Stack, TextField, Typography, + Button, } from '@mui/material'; import DeleteIcon from '@mui/icons-material/Delete'; import EditIcon from '@mui/icons-material/Edit'; import CheckIcon from '@mui/icons-material/Check'; import CloseIcon from '@mui/icons-material/Close'; import { ChangeEvent, useEffect, useState } from 'react'; -import { DEFAULT_BULK_NAME, DEFAULT_UNIT_TYPE, Product } from '../models/Product'; +import { DEFAULT_UNIT_TYPE, Product } from '../models/Product'; +import { BarcodeScannerView } from './BarcodeScannerView'; interface ProductRowProps { product: Product; @@ -23,7 +28,7 @@ interface ProductRowProps { updates: { name: string; category: string; - units_per_bulk?: number; + barcode?: string; }, ) => Promise | void; onDelete: (productId: string) => Promise | void; @@ -32,18 +37,19 @@ interface ProductRowProps { interface ProductFormState { name: string; category: string; - unitsPerBulk: string; + barcode: string; } const getInitialFormState = (product: Product): ProductFormState => ({ name: product.name, category: product.category, - unitsPerBulk: product.units_per_bulk?.toString() ?? '', + barcode: product.barcode ?? '', }); export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRowProps) => { const [isEditing, setIsEditing] = useState(false); const [formState, setFormState] = useState(() => getInitialFormState(product)); + const [isScannerOpen, setIsScannerOpen] = useState(false); useEffect(() => { setFormState(getInitialFormState(product)); @@ -58,7 +64,7 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow await onSave(product.id, { name: formState.name, category: formState.category, - units_per_bulk: formState.unitsPerBulk ? Number(formState.unitsPerBulk) : undefined, + barcode: formState.barcode || undefined, }); setIsEditing(false); }; @@ -87,13 +93,25 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow ))} - + {formState.barcode ? ( + setFormState((prev) => ({ ...prev, barcode: '' }))}> + Clear + + ), + }} + /> + ) : ( + + )} ) : ( @@ -103,9 +121,9 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow {product.category} • {product.unit_type || DEFAULT_UNIT_TYPE} - {product.units_per_bulk ? ( + {product.barcode ? ( - {product.units_per_bulk} per {product.bulk_name || DEFAULT_BULK_NAME} + Barcode: {product.barcode} ) : null} @@ -132,6 +150,17 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow )} + setIsScannerOpen(false)} fullWidth> + Scan Barcode + + { + setFormState((prev) => ({ ...prev, barcode: code })); + setIsScannerOpen(false); + }} + /> + + ); }; diff --git a/src/db/seed.ts b/src/db/seed.ts index 23f5c30..c262738 100644 --- a/src/db/seed.ts +++ b/src/db/seed.ts @@ -33,7 +33,6 @@ export const seedDatabase = async (db: StockFillDB) => { category: 'Drinks', unit_type: DEFAULT_UNIT_TYPE, bulk_name: DEFAULT_BULK_NAME, - units_per_bulk: 12, archived: false, created_at: now(), updated_at: now(), @@ -44,7 +43,6 @@ export const seedDatabase = async (db: StockFillDB) => { category: 'Snacks', unit_type: DEFAULT_UNIT_TYPE, bulk_name: DEFAULT_BULK_NAME, - units_per_bulk: 24, archived: false, created_at: now(), updated_at: now(), @@ -55,7 +53,6 @@ export const seedDatabase = async (db: StockFillDB) => { category: 'Confectionery', unit_type: DEFAULT_UNIT_TYPE, bulk_name: DEFAULT_BULK_NAME, - units_per_bulk: 32, archived: false, created_at: now(), updated_at: now(), diff --git a/src/hooks/useBarcodeScanner.ts b/src/hooks/useBarcodeScanner.ts index 29f7b9d..c238742 100644 --- a/src/hooks/useBarcodeScanner.ts +++ b/src/hooks/useBarcodeScanner.ts @@ -1,6 +1,9 @@ import { useEffect, useRef, useState } from 'react'; import { BrowserMultiFormatReader } from '@zxing/browser'; +type BarcodeDetection = { rawValue: string }; +type BarcodeDetectorClass = new () => { detect: (source: ImageBitmapSource) => Promise }; + export interface BarcodeResult { code?: string; error?: string; @@ -14,15 +17,20 @@ export const useBarcodeScanner = () => { const reader = new BrowserMultiFormatReader(); let active = true; let stop: (() => void) | undefined; + let currentVideoElement: HTMLVideoElement | null = null; const start = async () => { try { - if ('BarcodeDetector' in window) { - const detector = new (window as typeof window & { BarcodeDetector: any }).BarcodeDetector(); + const detectorClass = ( + window as typeof window & { BarcodeDetector?: BarcodeDetectorClass } + ).BarcodeDetector; + if (detectorClass) { + const detector = new detectorClass(); const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); if (videoRef.current) { videoRef.current.srcObject = stream; await videoRef.current.play(); + currentVideoElement = videoRef.current; } const scan = async () => { if (!active || !videoRef.current) return; @@ -58,8 +66,9 @@ export const useBarcodeScanner = () => { return () => { active = false; stop?.(); - if (videoRef.current?.srcObject) { - (videoRef.current.srcObject as MediaStream).getTracks().forEach((track) => track.stop()); + const stream = currentVideoElement?.srcObject as MediaStream | null; + if (stream) { + stream.getTracks().forEach((track) => track.stop()); } }; }, []); diff --git a/src/models/Product.ts b/src/models/Product.ts index 34595fa..809cfa1 100644 --- a/src/models/Product.ts +++ b/src/models/Product.ts @@ -4,7 +4,6 @@ export interface Product { category: string; unit_type: string; bulk_name?: string; - units_per_bulk?: number; barcode?: string; archived: boolean; created_at: number; diff --git a/src/screens/ManageProductsScreen.tsx b/src/screens/ManageProductsScreen.tsx index 1e80f08..f55ff81 100644 --- a/src/screens/ManageProductsScreen.tsx +++ b/src/screens/ManageProductsScreen.tsx @@ -6,6 +6,9 @@ import { TextField, Typography, InputAdornment, + Dialog, + DialogTitle, + DialogContent, } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; import { Link as RouterLink } from 'react-router-dom'; @@ -15,6 +18,7 @@ import { ProductRow } from '../components/ProductRow'; import { useCategories, useProducts } from '../hooks/dataHooks'; import { useDatabase } from '../context/DBProvider'; import { DEFAULT_BULK_NAME, DEFAULT_UNIT_TYPE } from '../models/Product'; +import { BarcodeScannerView } from '../components/BarcodeScannerView'; export const ManageProductsScreen = () => { const db = useDatabase(); @@ -23,7 +27,8 @@ export const ManageProductsScreen = () => { const [search, setSearch] = useState(''); const [name, setName] = useState(''); const [category, setCategory] = useState(''); - const [unitsPerBulk, setUnitsPerBulk] = useState(6); + const [barcode, setBarcode] = useState(''); + const [scannerOpen, setScannerOpen] = useState(false); const categoryOptions = useMemo(() => { const categoryNames = categories.map((item) => item.name); @@ -54,12 +59,13 @@ export const ManageProductsScreen = () => { category, unit_type: DEFAULT_UNIT_TYPE, bulk_name: DEFAULT_BULK_NAME, - units_per_bulk: unitsPerBulk, + barcode: barcode || undefined, archived: false, created_at: Date.now(), updated_at: Date.now(), }); setName(''); + setBarcode(''); }; const updateProduct = async ( @@ -67,7 +73,7 @@ export const ManageProductsScreen = () => { updates: { name: string; category: string; - units_per_bulk?: number; + barcode?: string; }, ) => { await db.products.update(productId, { @@ -113,12 +119,24 @@ export const ManageProductsScreen = () => { ))} - setUnitsPerBulk(Number(event.target.value))} - /> + {barcode ? ( + setBarcode(event.target.value)} + InputProps={{ + endAdornment: ( + + ), + }} + /> + ) : ( + + )} @@ -133,6 +151,17 @@ export const ManageProductsScreen = () => { /> ))} + setScannerOpen(false)} fullWidth> + Scan Barcode + + { + setBarcode(code); + setScannerOpen(false); + }} + /> + + ); };