modified: public/templates/products_template.csv
modified: src/components/ProductRow.test.tsx modified: src/components/ProductRow.tsx modified: src/screens/ImportExportScreen.tsx modified: src/screens/ManageProductsScreen.tsx modified: src/services/importExportService.ts
This commit is contained in:
1 parent
ed923b9363
commit
f9a093a39d
6 files changed
+224
-448
No files matched your search
@@ -17,12 +17,21 @@ const product: Product = {
|
||||
|
||||
const categories = ['Drinks', 'Snacks'];
|
||||
|
||||
// Provide a categoriesById Map for the new required prop.
|
||||
// The test product uses category 'Drinks' (a name), so mapping name->name is fine.
|
||||
// If you had an ID in the product, this map could be id->name.
|
||||
const categoriesById = new Map<string, string>([
|
||||
['Drinks', 'Drinks'],
|
||||
['Snacks', 'Snacks'],
|
||||
]);
|
||||
|
||||
describe('ProductRow', () => {
|
||||
it('shows name, category, and barcode without unit text in read-only mode', () => {
|
||||
render(
|
||||
<ProductRow
|
||||
product={product}
|
||||
categories={categories}
|
||||
categoriesById={categoriesById}
|
||||
onSave={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>,
|
||||
@@ -39,7 +48,7 @@ describe('ProductRow', () => {
|
||||
const onSave = vi.fn();
|
||||
|
||||
render(
|
||||
<ProductRow product={product} categories={categories} onSave={onSave} onDelete={vi.fn()} />,
|
||||
<ProductRow product={product} categories={categories} categoriesById={categoriesById} onSave={onSave} onDelete={vi.fn()} />,
|
||||
);
|
||||
|
||||
await user.click(screen.getByLabelText(/edit sparkling water/i));
|
||||
@@ -59,12 +68,12 @@ describe('ProductRow', () => {
|
||||
|
||||
it('surfaces validation errors from duplicate constraints', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onSave = vi.fn().mockRejectedValueOnce(Object.assign(new Error('dup'), { name: 'DuplicateNameError' }))
|
||||
const onSave = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(Object.assign(new Error('dup'), { name: 'DuplicateNameError' }))
|
||||
.mockRejectedValueOnce(Object.assign(new Error('dup'), { name: 'DuplicateBarcodeError' }));
|
||||
|
||||
render(
|
||||
<ProductRow product={product} categories={categories} onSave={onSave} onDelete={vi.fn()} />,
|
||||
);
|
||||
render(<ProductRow product={product} categories={categories} categoriesById={categoriesById} onSave={onSave} onDelete={vi.fn()} />);
|
||||
|
||||
await user.click(screen.getByLabelText(/edit sparkling water/i));
|
||||
await user.clear(screen.getByLabelText(/name/i));
|
||||
@@ -82,9 +91,7 @@ describe('ProductRow', () => {
|
||||
it('allows clearing and scanning a new barcode', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<ProductRow product={product} categories={categories} onSave={vi.fn()} onDelete={vi.fn()} />,
|
||||
);
|
||||
render(<ProductRow product={product} categories={categories} categoriesById={categoriesById} onSave={vi.fn()} onDelete={vi.fn()} />);
|
||||
|
||||
await user.click(screen.getByLabelText(/edit sparkling water/i));
|
||||
await user.click(screen.getByRole('button', { name: /clear/i }));
|
||||
|
||||
@@ -21,12 +21,15 @@ import { BarcodeScannerView } from './BarcodeScannerView';
|
||||
|
||||
interface ProductRowProps {
|
||||
product: Product;
|
||||
// list of category display names for the select
|
||||
categories: string[];
|
||||
// map of category id -> category name, used to resolve ids to names
|
||||
categoriesById: Map<string, string>;
|
||||
onSave: (
|
||||
productId: string,
|
||||
updates: {
|
||||
name: string;
|
||||
category: string;
|
||||
category: string; // this is the *name* when passed back to parent
|
||||
barcode?: string;
|
||||
},
|
||||
) => Promise<void> | void;
|
||||
@@ -39,22 +42,23 @@ interface ProductFormState {
|
||||
barcode: string;
|
||||
}
|
||||
|
||||
const getInitialFormState = (product: Product): ProductFormState => ({
|
||||
const getInitialFormState = (product: Product, categoriesById: Map<string, string>): ProductFormState => ({
|
||||
name: product.name,
|
||||
category: product.category,
|
||||
// If product.category is an id, resolve to name; otherwise assume it is already a name
|
||||
category: categoriesById.get(product.category) ?? product.category ?? '',
|
||||
barcode: product.barcode ?? '',
|
||||
});
|
||||
|
||||
export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRowProps) => {
|
||||
export const ProductRow = ({ product, categories, categoriesById, onSave, onDelete }: ProductRowProps) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formState, setFormState] = useState<ProductFormState>(() => getInitialFormState(product));
|
||||
const [formState, setFormState] = useState<ProductFormState>(() => getInitialFormState(product, categoriesById));
|
||||
const [isScannerOpen, setIsScannerOpen] = useState(false);
|
||||
const [fieldErrors, setFieldErrors] = useState<{ name?: string; barcode?: string }>({});
|
||||
|
||||
useEffect(() => {
|
||||
setFormState(getInitialFormState(product));
|
||||
setFormState(getInitialFormState(product, categoriesById));
|
||||
setFieldErrors({});
|
||||
}, [product]);
|
||||
}, [product, categoriesById]);
|
||||
|
||||
const handleChange = (field: keyof ProductFormState) => (event: ChangeEvent<HTMLInputElement>) => {
|
||||
setFormState((prev) => ({ ...prev, [field]: event.target.value }));
|
||||
@@ -86,7 +90,7 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
setFormState(getInitialFormState(product));
|
||||
setFormState(getInitialFormState(product, categoriesById));
|
||||
setFieldErrors({});
|
||||
};
|
||||
|
||||
@@ -103,14 +107,7 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow
|
||||
error={Boolean(fieldErrors.name)}
|
||||
helperText={fieldErrors.name || undefined}
|
||||
/>
|
||||
<TextField
|
||||
select
|
||||
SelectProps={{ native: true }}
|
||||
label="Category"
|
||||
value={formState.category}
|
||||
onChange={handleChange('category')}
|
||||
size="small"
|
||||
>
|
||||
<TextField select SelectProps={{ native: true }} label="Category" value={formState.category} onChange={handleChange('category')} size="small">
|
||||
{categories.map((cat) => (
|
||||
<option key={cat} value={cat}>
|
||||
{cat}
|
||||
@@ -145,12 +142,7 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow
|
||||
</Button>
|
||||
)}
|
||||
<Stack direction="row" spacing={0.5} justifyContent="flex-end" alignItems="center">
|
||||
<IconButton
|
||||
aria-label={`Delete ${product.name}`}
|
||||
onClick={() => onDelete(product.id)}
|
||||
size="small"
|
||||
color="error"
|
||||
>
|
||||
<IconButton aria-label={`Delete ${product.name}`} onClick={() => onDelete(product.id)} size="small" color="error">
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton aria-label="Save product" onClick={handleSave} disabled={!formState.name} color="primary">
|
||||
@@ -169,7 +161,8 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow
|
||||
{product.name}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
{product.category}
|
||||
{/* Resolve id -> name for display */}
|
||||
{categoriesById.get(product.category) ?? product.category ?? ''}
|
||||
</Typography>
|
||||
</Stack>
|
||||
{product.barcode ? (
|
||||
|
||||
Reference in new issue
Block a user