modified: src/screens/ManageProductsScreen.tsx

This commit is contained in:
beatzaplenty committed 2025-11-25 13:46:04 +10:00
1 parent f61d078900
commit 0465683018
1 file changed
+179 -107
+179 -107
View File
@@ -93,48 +93,47 @@ export const ManageProductsScreen = () => {
[findNameConflict], [findNameConflict],
); );
const addProductToAutoLists = useCallback( const addProductToAutoLists = useCallback(
async (product: Product, timestamp: number) => { async (product: Product, timestamp: number) => {
const pickLists = await db.pickLists.toArray(); const pickLists = await db.pickLists.toArray();
const categoriesAll = await db.categories.toArray(); const categoriesAll = await db.categories.toArray();
const categoriesByName = new Map(categoriesAll.map((c) => [c.name, c.id])); const categoriesByName = new Map(categoriesAll.map((c) => [c.name, c.id]));
const eligibleLists = pickLists.filter((pickList) => const eligibleLists = pickLists.filter((pickList) =>
pickList.auto_add_new_products && Array.isArray(pickList.categories) pickList.auto_add_new_products && Array.isArray(pickList.categories)
? pickList.categories.some((catRef: string) => { ? pickList.categories.some((catRef: string) => {
// catRef can be an id or a name — resolve both // catRef can be an id or a name — resolve both
if (catRef === product.category) return true; if (catRef === product.category) return true;
const resolvedId = categoriesByName.get(catRef); const resolvedId = categoriesByName.get(catRef);
return resolvedId === product.category; return resolvedId === product.category;
}) })
: false, : false,
); );
if (eligibleLists.length === 0) return;
await Promise.all(
eligibleLists.map(async (pickList) => {
const existing = await db.pickItems
.where('pick_list_id')
.equals(pickList.id)
.filter((item) => item.product_id === product.id)
.first();
if (existing) return undefined;
return db.pickItems.add({
id: uuidv4(),
pick_list_id: pickList.id,
product_id: product.id,
quantity: 1,
is_carton: false,
status: 'pending',
created_at: timestamp,
updated_at: timestamp,
});
}),
);
},
[db.pickItems, db.pickLists, db.categories],
);
if (eligibleLists.length === 0) return;
await Promise.all(
eligibleLists.map(async (pickList) => {
const existing = await db.pickItems
.where('pick_list_id')
.equals(pickList.id)
.filter((item) => item.product_id === product.id)
.first();
if (existing) return undefined;
return db.pickItems.add({
id: uuidv4(),
pick_list_id: pickList.id,
product_id: product.id,
quantity: 1,
is_carton: false,
status: 'pending',
created_at: timestamp,
updated_at: timestamp,
});
}),
);
},
[db.pickItems, db.pickLists, db.categories],
);
useEffect(() => { useEffect(() => {
if (categoryOptions.length === 0) return; if (categoryOptions.length === 0) return;
@@ -204,43 +203,85 @@ const addProductToAutoLists = useCallback(
} }
const addProduct = async () => { const addProduct = async () => {
// Resolve selected category name -> id if possible, otherwise create // clear previous field-level errors
let categoryIdToSave: string; setNameError('');
let chosenCategoryObj = categories.find((c) => c.name === category); setBarcodeError('');
if (chosenCategoryObj) { // Basic guard (button is disabled when not provided but extra safety)
categoryIdToSave = chosenCategoryObj.id; if (!name || !category) {
} else { setFeedback({ text: 'Name and category are required.', severity: 'error' });
// create a category automatically (or you can prompt the user if you prefer) return;
const now = Date.now();
const newCatId = uuidv4();
await db.categories.add({ id: newCatId, name: category, created_at: now, updated_at: now });
categoryIdToSave = newCatId;
} }
const timestamp = Date.now(); const timestamp = Date.now();
const productId = uuidv4(); const productId = uuidv4();
const newProduct: Product = { try {
id: productId, // Run uniqueness checks inside the try so we can handle the errors
name, await assertUniqueName(name);
category: categoryIdToSave, await assertUniqueBarcode(barcode);
unit_type: DEFAULT_UNIT_TYPE,
bulk_name: DEFAULT_BULK_NAME,
barcode: barcode || undefined,
archived: false,
created_at: timestamp,
updated_at: timestamp,
};
await db.transaction('rw', db.products, db.pickLists, db.pickItems, async () => { // Use a transaction that includes categories too, and make category creation atomic
await db.products.add(newProduct); await db.transaction(
await addProductToAutoLists(newProduct, timestamp); 'rw',
}); db.categories,
setName(''); db.products,
setBarcode(''); db.pickLists,
setNameError(''); db.pickItems,
setFeedback({ text: 'Product added.', severity: 'success' }); async () => {
// Determine or create the category within the transaction
let categoryIdToSave: string;
const existingCategory = await db.categories.where('name').equals(category).first();
if (existingCategory) {
categoryIdToSave = existingCategory.id;
} else {
const newCatId = uuidv4();
const now = Date.now();
await db.categories.add({ id: newCatId, name: category, created_at: now, updated_at: now });
categoryIdToSave = newCatId;
}
const newProduct: Product = {
id: productId,
name: name.trim(),
category: categoryIdToSave,
unit_type: DEFAULT_UNIT_TYPE,
bulk_name: DEFAULT_BULK_NAME,
barcode: barcode || undefined,
archived: false,
created_at: timestamp,
updated_at: timestamp,
};
await db.products.add(newProduct);
// add product to auto-add pick lists (this uses db.* but will execute within the same transaction)
await addProductToAutoLists(newProduct, timestamp);
},
);
// Clear inputs & show success feedback
setName('');
setBarcode('');
setNameError('');
setBarcodeError('');
setFeedback({ text: 'Product added.', severity: 'success' });
} catch (err: any) {
console.error('Failed to add product', err);
// Provide field-level errors for duplicates
if (err?.name === 'DuplicateNameError') {
setNameError(err.message || 'A product with this name already exists.');
return;
}
if (err?.name === 'DuplicateBarcodeError') {
setBarcodeError(err.message || 'This barcode is already assigned to another product.');
return;
}
// Generic DB failure
setFeedback({ text: `Failed to add product: ${err?.message ?? String(err)}`, severity: 'error' });
}
}; };
const updateProduct = async ( const updateProduct = async (
@@ -251,46 +292,77 @@ const addProductToAutoLists = useCallback(
barcode?: string; barcode?: string;
}, },
) => { ) => {
await assertUniqueName(updates.name, productId); // Clear previous errors
await assertUniqueBarcode(updates.barcode, productId); setNameError('');
setBarcodeError('');
const existing = await db.products.get(productId); try {
if (!existing) return; await assertUniqueName(updates.name, productId);
await assertUniqueBarcode(updates.barcode, productId);
// Map the provided category name back to the id (if it exists), or create one // Get existing product (we'll still update inside a transaction)
let categoryIdToSave = updates.category; const existing = await db.products.get(productId);
const matchingCategory = categories.find((c) => c.name === updates.category); if (!existing) return;
if (matchingCategory) {
categoryIdToSave = matchingCategory.id; const normalizedName = updates.name.trim();
} else { const oldNameKey = existing.name.trim().toLowerCase();
// If updates.category already looks like an id, keep it. Otherwise create a new category
const isExistingId = categories.some((c) => c.id === updates.category); // Do the category resolution/creation and product update in one transaction
if (!isExistingId) { await db.transaction(
const newCatId = uuidv4(); 'rw',
const now = Date.now(); db.categories,
await db.categories.add({ id: newCatId, name: updates.category, created_at: now, updated_at: now }); db.products,
categoryIdToSave = newCatId; async () => {
} // Map the provided category name back to the id (if it exists), or create one
let categoryIdToSave = updates.category;
const matchingCategory = await db.categories.where('name').equals(updates.category).first();
if (matchingCategory) {
categoryIdToSave = matchingCategory.id;
} else {
// If updates.category already looks like an id, check it exists
const isExistingId = await db.categories.get(updates.category);
if (!isExistingId) {
const newCatId = uuidv4();
const now = Date.now();
await db.categories.add({ id: newCatId, name: updates.category, created_at: now, updated_at: now });
categoryIdToSave = newCatId;
} else {
// updates.category was an id and exists — no change
categoryIdToSave = updates.category;
}
}
const updatedProduct: Product = {
...existing,
...updates,
name: normalizedName,
category: categoryIdToSave,
unit_type: DEFAULT_UNIT_TYPE,
bulk_name: DEFAULT_BULK_NAME,
updated_at: Date.now(),
};
await db.products.put(updatedProduct);
// Remove duplicates that used to have the same old name
await db.products
.filter((product) => product.id !== productId && product.name.trim().toLowerCase() === oldNameKey)
.delete();
},
);
setFeedback({ text: 'Product updated.', severity: 'success' });
} catch (err: any) {
console.error('Failed to update product', err);
if (err?.name === 'DuplicateNameError') {
setNameError(err.message || 'A product with this name already exists.');
return;
} }
if (err?.name === 'DuplicateBarcodeError') {
setBarcodeError(err.message || 'This barcode is already assigned to another product.');
const normalizedName = updates.name.trim(); return;
const oldNameKey = existing.name.trim().toLowerCase(); }
const updatedProduct: Product = { setFeedback({ text: `Failed to update product: ${err?.message ?? String(err)}`, severity: 'error' });
...existing, }
...updates,
name: normalizedName,
category: categoryIdToSave,
unit_type: DEFAULT_UNIT_TYPE,
bulk_name: DEFAULT_BULK_NAME,
updated_at: Date.now(),
};
await db.products.put(updatedProduct);
await db.products
.filter((product) => product.id !== productId && product.name.trim().toLowerCase() === oldNameKey)
.delete();
setFeedback({ text: 'Product updated.', severity: 'success' });
}; };
const deleteProduct = async (productId: string) => { const deleteProduct = async (productId: string) => {