diff --git a/.vscode/launch.json b/.vscode/launch.json index 147923c..f5af679 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -3,19 +3,17 @@ "configurations": [ { "name": "Launch Vite Dev Server", - "type": "pwa-chrome", + "type": "node-terminal", "request": "launch", - "url": "http://localhost:5173/", - "webRoot": "${workspaceFolder}/src", - "preLaunchTask": "npm: dev server" + "command": "npm run dev", + "cwd": "${workspaceFolder}" }, { "name": "Launch Preview (Test) Server", - "type": "pwa-chrome", + "type": "node-terminal", "request": "launch", - "url": "http://localhost:4173/", - "webRoot": "${workspaceFolder}/src", - "preLaunchTask": "npm: preview server" + "command": "npm run preview", + "cwd": "${workspaceFolder}" } ] } diff --git a/src/App.tsx b/src/App.tsx index e4c2e33..5d2ee4e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { CssBaseline, ThemeProvider, createTheme } from '@mui/material'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { DBProvider } from './context/DBProvider'; +import { AppLayout } from './components/AppLayout'; import { HomeScreen } from './screens/HomeScreen'; import { StartPickListScreen } from './screens/StartPickListScreen'; import { PickListsScreen } from './screens/PickListsScreen'; @@ -24,14 +25,16 @@ const AppRoutes = () => { useServiceWorker(); return ( - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + ); }; diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx new file mode 100644 index 0000000..da4fd0c --- /dev/null +++ b/src/components/AppLayout.tsx @@ -0,0 +1,12 @@ +import { Box } from '@mui/material'; +import { Outlet } from 'react-router-dom'; +import { NavigationBar } from './NavigationBar'; + +export const AppLayout = () => ( + + + + + + +); diff --git a/src/components/NavigationBar.tsx b/src/components/NavigationBar.tsx new file mode 100644 index 0000000..b7cc3fe --- /dev/null +++ b/src/components/NavigationBar.tsx @@ -0,0 +1,54 @@ +import { AppBar, Box, Button, Toolbar, Typography } from '@mui/material'; +import { Link as RouterLink, useLocation } from 'react-router-dom'; + +const navLinks = [ + { to: '/', label: 'Home' }, + { to: '/start', label: 'Start' }, + { to: '/pick-lists', label: 'Pick Lists' }, + { to: '/products', label: 'Products' }, + { to: '/areas', label: 'Areas' }, + { to: '/scan', label: 'Scan' }, +]; + +const isActivePath = (currentPath: string, target: string) => + currentPath === target || currentPath.startsWith(`${target}/`); + +export const NavigationBar = () => { + const location = useLocation(); + + return ( + + + + StockFill + + + {navLinks.map((link) => { + const active = isActivePath(location.pathname, link.to); + return ( + + {link.label} + + ); + })} + + + + ); +}; diff --git a/src/components/ProductRow.tsx b/src/components/ProductRow.tsx index 14cc8cb..d494209 100644 --- a/src/components/ProductRow.tsx +++ b/src/components/ProductRow.tsx @@ -1,26 +1,161 @@ -import { Card, CardContent, Stack, Typography } from '@mui/material'; +import { + Card, + CardActions, + CardContent, + IconButton, + MenuItem, + Stack, + TextField, + Typography, +} 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 { Product } from '../models/Product'; interface ProductRowProps { product: Product; + categories: string[]; + onSave: ( + productId: string, + updates: { + name: string; + category: string; + unit_type: string; + bulk_name?: string; + units_per_bulk?: number; + }, + ) => Promise | void; + onDelete: (productId: string) => Promise | void; } -export const ProductRow = ({ product }: ProductRowProps) => ( - - - - - {product.name} - - {product.category} • {product.unit_type} - - - {product.bulk_name && product.units_per_bulk ? ( - - {product.units_per_bulk} per {product.bulk_name} - - ) : null} - - - -); +interface ProductFormState { + name: string; + category: string; + unitType: string; + bulkName: string; + unitsPerBulk: string; +} + +const getInitialFormState = (product: Product): ProductFormState => ({ + name: product.name, + category: product.category, + unitType: product.unit_type, + bulkName: product.bulk_name ?? '', + unitsPerBulk: product.units_per_bulk?.toString() ?? '', +}); + +export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRowProps) => { + const [isEditing, setIsEditing] = useState(false); + const [formState, setFormState] = useState(() => getInitialFormState(product)); + + useEffect(() => { + setFormState(getInitialFormState(product)); + }, [product]); + + const handleChange = (field: keyof ProductFormState) => (event: ChangeEvent) => { + setFormState((prev) => ({ ...prev, [field]: event.target.value })); + }; + + const handleSave = async () => { + if (!formState.name) return; + await onSave(product.id, { + name: formState.name, + category: formState.category, + unit_type: formState.unitType, + bulk_name: formState.bulkName || undefined, + units_per_bulk: formState.unitsPerBulk ? Number(formState.unitsPerBulk) : undefined, + }); + setIsEditing(false); + }; + + const handleCancel = () => { + setIsEditing(false); + setFormState(getInitialFormState(product)); + }; + + return ( + + + {isEditing ? ( + + + + {categories.map((cat) => ( + + {cat} + + ))} + + + + + + + + ) : ( + + + {product.name} + + {product.category} • {product.unit_type} + + + {product.bulk_name && product.units_per_bulk ? ( + + {product.units_per_bulk} per {product.bulk_name} + + ) : null} + + )} + + + {isEditing ? ( + <> + + + + + + + > + ) : ( + <> + setIsEditing(true)}> + + + onDelete(product.id)}> + + + > + )} + + + ); +}; diff --git a/src/screens/ManageAreasScreen.tsx b/src/screens/ManageAreasScreen.tsx index c565550..61cd5db 100644 --- a/src/screens/ManageAreasScreen.tsx +++ b/src/screens/ManageAreasScreen.tsx @@ -1,4 +1,18 @@ -import { Button, Container, List, ListItem, ListItemText, Stack, TextField, Typography } from '@mui/material'; +import { + Button, + Container, + IconButton, + List, + ListItem, + ListItemText, + Stack, + TextField, + Typography, +} 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 { useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import { useAreas } from '../hooks/dataHooks'; @@ -8,6 +22,8 @@ export const ManageAreasScreen = () => { const db = useDatabase(); const areas = useAreas(); const [name, setName] = useState(''); + const [editingAreaId, setEditingAreaId] = useState(null); + const [editName, setEditName] = useState(''); const addArea = async () => { if (!name) return; @@ -15,6 +31,30 @@ export const ManageAreasScreen = () => { setName(''); }; + const startEditing = (areaId: string, currentName: string) => { + setEditingAreaId(areaId); + setEditName(currentName); + }; + + const saveArea = async () => { + if (!editingAreaId || !editName) return; + await db.areas.update(editingAreaId, { name: editName, updated_at: Date.now() }); + setEditingAreaId(null); + setEditName(''); + }; + + const cancelEditing = () => { + setEditingAreaId(null); + setEditName(''); + }; + + const deleteArea = async (areaId: string) => { + await db.areas.delete(areaId); + if (editingAreaId === areaId) { + cancelEditing(); + } + }; + return ( @@ -30,7 +70,42 @@ export const ManageAreasScreen = () => { {areas.map((area) => ( - + {editingAreaId === area.id ? ( + + setEditName(event.target.value)} + /> + + + + + + + + ) : ( + + + + startEditing(area.id, area.name)} + aria-label={`Edit ${area.name}`} + size="small" + > + + + deleteArea(area.id)} + aria-label={`Delete ${area.name}`} + size="small" + > + + + + + )} ))} diff --git a/src/screens/ManageProductsScreen.tsx b/src/screens/ManageProductsScreen.tsx index 9a4eaa4..451fd15 100644 --- a/src/screens/ManageProductsScreen.tsx +++ b/src/screens/ManageProductsScreen.tsx @@ -50,6 +50,23 @@ export const ManageProductsScreen = () => { setName(''); }; + const updateProduct = async ( + productId: string, + updates: { + name: string; + category: string; + unit_type: string; + bulk_name?: string; + units_per_bulk?: number; + }, + ) => { + await db.products.update(productId, { ...updates, updated_at: Date.now() }); + }; + + const deleteProduct = async (productId: string) => { + await db.products.delete(productId); + }; + return ( @@ -85,7 +102,13 @@ export const ManageProductsScreen = () => { {filtered.map((product) => ( - + ))}