deleted: public/templates/areas_template.csv

deleted:    public/templates/categories_template.csv
	deleted:    public/templates/pickitems_template.csv
	deleted:    public/templates/picklists_template.csv
	modified:   src/db/migrations.ts
	modified:   src/screens/ManageCategoriesScreen.tsx
	modified:   src/screens/ManageProductsScreen.tsx
This commit is contained in:
2025-11-25 11:41:15 +10:00
parent f9a093a39d
commit f61d078900
7 changed files with 218 additions and 106 deletions
-3
View File
@@ -1,3 +0,0 @@
id,name,created_at,updated_at
# id optional — you can use empty for new rows. created/updated optional
Area A,,
1 id,name,created_at,updated_at
2 # id optional — you can use empty for new rows. created/updated optional
3 Area A,,
-2
View File
@@ -1,2 +0,0 @@
id,name,created_at,updated_at
Clothing,,
1 id,name,created_at,updated_at
2 Clothing,,
-2
View File
@@ -1,2 +0,0 @@
id,pick_list_id,pick_list_name,product_id,product_name,quantity,is_carton,status,created_at,updated_at
,,"My Pick List",,"Blue T-Shirt",2,false,pending,,
1 id pick_list_id pick_list_name product_id product_name quantity is_carton status created_at updated_at
2 My Pick List Blue T-Shirt 2 false pending
-2
View File
@@ -1,2 +0,0 @@
id,area_id,area_name,created_at,completed_at,notes,categories,auto_add_new_products
,,"Area A",,,"","Clothing;Accessories",false
1 id area_id area_name created_at completed_at notes categories auto_add_new_products
2 Area A Clothing;Accessories false
+75 -1
View File
@@ -1,6 +1,80 @@
// src/db/migrations.ts
import { StockFillDB } from './index'; import { StockFillDB } from './index';
import { v4 as uuidv4 } from 'uuid';
export const applyMigrations = async (db: StockFillDB) => { export const applyMigrations = async (db: StockFillDB) => {
// Future migrations can be added here. // Ensure DB is open and ready
await db.open(); await db.open();
// Run a single transaction that normalizes products and pickLists
await db.transaction('rw', db.categories, db.products, db.pickLists, async () => {
const now = Date.now();
// Load categories (existing)
const categories = await db.categories.toArray();
const categoriesByName = new Map(categories.map((c) => [c.name, c.id]));
const categoriesById = new Map(categories.map((c) => [c.id, c.name]));
// 1) Normalize products: category -> categoryId
const products = await db.products.toArray();
await Promise.all(
products.map(async (product) => {
const cat = product.category ?? '';
// Skip empty
if (!cat) return;
// If already an id that matches a known category, skip
if (categoriesById.has(cat)) return;
// If it's a name that matches an existing category, update
const matchingId = categoriesByName.get(cat);
if (matchingId) {
await db.products.update(product.id, { category: matchingId });
return;
}
// Otherwise: create a new category with this name and use its id
const newCatId = uuidv4();
await db.categories.add({
id: newCatId,
name: cat,
created_at: now,
updated_at: now,
});
categoriesByName.set(cat, newCatId);
categoriesById.set(newCatId, cat);
await db.products.update(product.id, { category: newCatId });
}),
);
// Refresh categories maps (in case we added new ones)
const updatedCategories = await db.categories.toArray();
const updatedByName = new Map(updatedCategories.map((c) => [c.name, c.id]));
const updatedById = new Map(updatedCategories.map((c) => [c.id, c.name]));
// 2) Normalize pickLists.categories (array) to hold ids (not names)
const pickLists = await db.pickLists.toArray();
await Promise.all(
pickLists.map(async (pl) => {
if (!Array.isArray((pl as any).categories)) return;
const newCats = (pl as any).categories.map((entry: string) => {
// If the entry is already an id we know, keep it
if (updatedById.has(entry)) return entry;
// If entry is a name, return its id (if exists)
const id = updatedByName.get(entry);
if (id) return id;
// If it's neither, keep as-is (or optionally create category)
return entry;
});
// Update only if changed
if (JSON.stringify(newCats) !== JSON.stringify((pl as any).categories)) {
await db.pickLists.update(pl.id, { categories: newCats });
}
}),
);
});
}; };
+39 -11
View File
@@ -28,13 +28,17 @@ export const ManageCategoriesScreen = () => {
const [editingCategoryId, setEditingCategoryId] = useState<string | null>(null); const [editingCategoryId, setEditingCategoryId] = useState<string | null>(null);
const [editName, setEditName] = useState(''); const [editName, setEditName] = useState('');
const [feedback, setFeedback] = useState<{ text: string; severity: AlertColor } | null>(null); const [feedback, setFeedback] = useState<{ text: string; severity: AlertColor } | null>(null);
const categoriesById = useMemo(() => new Map(categories.map((c) => [c.id, c.name])), [categories]);
const usageByCategory = useMemo(() => { const usageByCategory = useMemo(() => {
return products.reduce<Record<string, number>>((acc, product) => { return products.reduce<Record<string, number>>((acc, product) => {
acc[product.category] = (acc[product.category] ?? 0) + 1; const categoryName = categoriesById.get(product.category) ?? product.category ?? '';
if (!categoryName) return acc;
acc[categoryName] = (acc[categoryName] ?? 0) + 1;
return acc; return acc;
}, {}); }, {});
}, [products]); }, [products, categoriesById]);
const addCategory = async () => { const addCategory = async () => {
const trimmed = name.trim(); const trimmed = name.trim();
@@ -55,7 +59,7 @@ export const ManageCategoriesScreen = () => {
setFeedback(null); setFeedback(null);
}; };
const saveCategory = async () => { const saveCategory = async () => {
if (!editingCategoryId) return; if (!editingCategoryId) return;
const trimmed = editName.trim(); const trimmed = editName.trim();
if (!trimmed) return; if (!trimmed) return;
@@ -71,18 +75,36 @@ export const ManageCategoriesScreen = () => {
return; return;
} }
await db.transaction('rw', db.categories, db.products, async () => { await db.transaction('rw', db.categories, db.products, db.pickLists, async () => {
// Update category name
await db.categories.update(editingCategoryId, { name: trimmed, updated_at: Date.now() }); await db.categories.update(editingCategoryId, { name: trimmed, updated_at: Date.now() });
// For backward-compat products that stored category as the old name,
// update them to reference the new name OR ideally, to the id.
// We map products that still have category === oldName to the new name value.
// (If you ran the normalization migration earlier, most products will already have ids.)
await db.products await db.products
.where('category') .where('category')
.equals(category.name) .equals(category.name)
.modify({ category: trimmed, updated_at: Date.now() }); .modify({ category: trimmed, updated_at: Date.now() });
// Update pickLists that used the old category name
const pickLists = await db.pickLists.toArray();
await Promise.all(
pickLists.map(async (pl) => {
if (!Array.isArray((pl as any).categories)) return;
const needs = (pl as any).categories.includes(category.name);
if (!needs) return;
const updated = (pl as any).categories.map((c: string) => (c === category.name ? trimmed : c));
await db.pickLists.update(pl.id, { categories: updated });
}),
);
}); });
setEditingCategoryId(null); setEditingCategoryId(null);
setEditName(''); setEditName('');
setFeedback({ text: 'Category updated. Linked products were refreshed.', severity: 'success' }); setFeedback({ text: 'Category updated. Linked products were refreshed.', severity: 'success' });
}; };
const cancelEditing = () => { const cancelEditing = () => {
setEditingCategoryId(null); setEditingCategoryId(null);
@@ -90,8 +112,14 @@ export const ManageCategoriesScreen = () => {
setFeedback(null); setFeedback(null);
}; };
const deleteCategory = async (categoryId: string, categoryName: string) => { const deleteCategory = async (categoryId: string, categoryName: string) => {
const usageCount = usageByCategory[categoryName] ?? 0; // Count products referencing either the id or the name (legacy)
const [countById, countByName] = await Promise.all([
db.products.where('category').equals(categoryId).count(),
db.products.where('category').equals(categoryName).count(),
]);
const usageCount = countById + countByName;
if (usageCount > 0) { if (usageCount > 0) {
setFeedback({ setFeedback({
text: `Cannot delete '${categoryName}' while ${usageCount} product(s) use it. Update those products first.`, text: `Cannot delete '${categoryName}' while ${usageCount} product(s) use it. Update those products first.`,
@@ -99,12 +127,12 @@ export const ManageCategoriesScreen = () => {
}); });
return; return;
} }
await db.categories.delete(categoryId); await db.categories.delete(categoryId);
if (editingCategoryId === categoryId) { if (editingCategoryId === categoryId) cancelEditing();
cancelEditing();
}
setFeedback({ text: 'Category deleted.', severity: 'success' }); setFeedback({ text: 'Category deleted.', severity: 'success' });
}; };
return ( return (
<Container sx={{ py: 4 }}> <Container sx={{ py: 4 }}>
+43 -24
View File
@@ -93,12 +93,23 @@ 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 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.categories.includes(product.category) : false, pickList.auto_add_new_products && Array.isArray(pickList.categories)
? pickList.categories.some((catRef: string) => {
// catRef can be an id or a name — resolve both
if (catRef === product.category) return true;
const resolvedId = categoriesByName.get(catRef);
return resolvedId === product.category;
})
: false,
); );
if (eligibleLists.length === 0) return; if (eligibleLists.length === 0) return;
await Promise.all( await Promise.all(
eligibleLists.map(async (pickList) => { eligibleLists.map(async (pickList) => {
@@ -121,8 +132,9 @@ export const ManageProductsScreen = () => {
}), }),
); );
}, },
[db.pickItems, db.pickLists], [db.pickItems, db.pickLists, db.categories],
); );
useEffect(() => { useEffect(() => {
if (categoryOptions.length === 0) return; if (categoryOptions.length === 0) return;
@@ -192,28 +204,23 @@ export const ManageProductsScreen = () => {
} }
const addProduct = async () => { const addProduct = async () => {
if (!name || !category) return; // Resolve selected category name -> id if possible, otherwise create
try { let categoryIdToSave: string;
await assertUniqueName(name); let chosenCategoryObj = categories.find((c) => c.name === category);
await assertUniqueBarcode(barcode || undefined);
} catch (error) { if (chosenCategoryObj) {
if (error instanceof Error && error.name === 'DuplicateNameError') { categoryIdToSave = chosenCategoryObj.id;
setNameError(error.message); } else {
return; // create a category automatically (or you can prompt the user if you prefer)
} const now = Date.now();
if (error instanceof Error && error.name === 'DuplicateBarcodeError') { const newCatId = uuidv4();
setBarcodeError(error.message); await db.categories.add({ id: newCatId, name: category, created_at: now, updated_at: now });
return; categoryIdToSave = newCatId;
}
throw error;
} }
const timestamp = Date.now(); const timestamp = Date.now();
const productId = uuidv4(); const productId = uuidv4();
// Resolve selected category name -> id if possible
const chosenCategoryObj = categories.find((c) => c.name === category);
const categoryIdToSave = chosenCategoryObj ? chosenCategoryObj.id : category || '';
const newProduct: Product = { const newProduct: Product = {
id: productId, id: productId,
name, name,
@@ -250,10 +257,22 @@ export const ManageProductsScreen = () => {
const existing = await db.products.get(productId); const existing = await db.products.get(productId);
if (!existing) return; if (!existing) return;
// Map the provided category name back to the id (if it exists) // Map the provided category name back to the id (if it exists), or create one
let categoryIdToSave = updates.category; let categoryIdToSave = updates.category;
const matchingCategory = categories.find((c) => c.name === updates.category); const matchingCategory = categories.find((c) => c.name === updates.category);
if (matchingCategory) categoryIdToSave = matchingCategory.id; if (matchingCategory) {
categoryIdToSave = matchingCategory.id;
} else {
// If updates.category already looks like an id, keep it. Otherwise create a new category
const isExistingId = categories.some((c) => c.id === 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;
}
}
const normalizedName = updates.name.trim(); const normalizedName = updates.name.trim();
const oldNameKey = existing.name.trim().toLowerCase(); const oldNameKey = existing.name.trim().toLowerCase();