feat(shopping): listShoppingList mit Aggregation + Skalierung
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m14s
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m14s
This commit is contained in:
@@ -67,7 +67,9 @@ export function setCartServings(
|
||||
).run(servings, recipeId);
|
||||
}
|
||||
|
||||
export function listShoppingList(db: Database.Database): ShoppingListSnapshot {
|
||||
export function listShoppingList(
|
||||
db: Database.Database
|
||||
): ShoppingListSnapshot {
|
||||
const recipes = db
|
||||
.prepare(
|
||||
`SELECT cr.recipe_id, r.title, r.image_path, cr.servings,
|
||||
@@ -77,9 +79,31 @@ export function listShoppingList(db: Database.Database): ShoppingListSnapshot {
|
||||
ORDER BY cr.added_at ASC`
|
||||
)
|
||||
.all() as ShoppingCartRecipe[];
|
||||
// TODO(Task 6): rows + uncheckedCount are populated by the aggregation query.
|
||||
// Until then, callers must not rely on these fields.
|
||||
return { recipes, rows: [], uncheckedCount: 0 };
|
||||
|
||||
const rows = db
|
||||
.prepare(
|
||||
`SELECT
|
||||
LOWER(TRIM(i.name)) AS name_key,
|
||||
LOWER(TRIM(COALESCE(i.unit, ''))) AS unit_key,
|
||||
MIN(i.name) AS display_name,
|
||||
MIN(i.unit) AS display_unit,
|
||||
SUM(i.quantity * cr.servings * 1.0 / COALESCE(r.servings_default, cr.servings)) AS total_quantity,
|
||||
GROUP_CONCAT(DISTINCT r.title) AS from_recipes,
|
||||
EXISTS(
|
||||
SELECT 1 FROM shopping_cart_check c
|
||||
WHERE c.name_key = LOWER(TRIM(i.name))
|
||||
AND c.unit_key = LOWER(TRIM(COALESCE(i.unit, '')))
|
||||
) AS checked
|
||||
FROM shopping_cart_recipe cr
|
||||
JOIN recipe r ON r.id = cr.recipe_id
|
||||
JOIN ingredient i ON i.recipe_id = r.id
|
||||
GROUP BY name_key, unit_key
|
||||
ORDER BY checked ASC, display_name COLLATE NOCASE`
|
||||
)
|
||||
.all() as ShoppingListRow[];
|
||||
|
||||
const uncheckedCount = rows.reduce((n, r) => n + (r.checked ? 0 : 1), 0);
|
||||
return { recipes, rows, uncheckedCount };
|
||||
}
|
||||
|
||||
export function toggleCheck(
|
||||
|
||||
@@ -103,3 +103,73 @@ describe('setCartServings', () => {
|
||||
expect(() => setCartServings(db, id, -3)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('listShoppingList aggregation', () => {
|
||||
it('aggregates same name+unit across recipes', () => {
|
||||
const db = openInMemoryForTest();
|
||||
const a = insertRecipe(db, recipe({
|
||||
title: 'Carbonara', servings_default: 4,
|
||||
ingredients: [{ position: 1, quantity: 200, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }]
|
||||
}));
|
||||
const b = insertRecipe(db, recipe({
|
||||
title: 'Lasagne', servings_default: 4,
|
||||
ingredients: [{ position: 1, quantity: 200, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }]
|
||||
}));
|
||||
addRecipeToCart(db, a, null, 4);
|
||||
addRecipeToCart(db, b, null, 4);
|
||||
const rows = listShoppingList(db).rows;
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].name_key).toBe('mehl');
|
||||
expect(rows[0].unit_key).toBe('g');
|
||||
expect(rows[0].total_quantity).toBe(400);
|
||||
expect(rows[0].from_recipes).toContain('Carbonara');
|
||||
expect(rows[0].from_recipes).toContain('Lasagne');
|
||||
});
|
||||
|
||||
it('keeps different units as separate rows', () => {
|
||||
const db = openInMemoryForTest();
|
||||
const id = insertRecipe(db, recipe({
|
||||
servings_default: 4,
|
||||
ingredients: [
|
||||
{ position: 1, quantity: 100, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null },
|
||||
{ position: 2, quantity: 1, unit: 'Pck', name: 'Mehl', note: null, raw_text: '', section_heading: null }
|
||||
]
|
||||
}));
|
||||
addRecipeToCart(db, id, null, 4);
|
||||
const rows = listShoppingList(db).rows;
|
||||
expect(rows).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('scales quantities by servings/servings_default', () => {
|
||||
const db = openInMemoryForTest();
|
||||
const id = insertRecipe(db, recipe({
|
||||
servings_default: 4,
|
||||
ingredients: [{ position: 1, quantity: 200, unit: 'g', name: 'Mehl', note: null, raw_text: '', section_heading: null }]
|
||||
}));
|
||||
addRecipeToCart(db, id, null, 2);
|
||||
expect(listShoppingList(db).rows[0].total_quantity).toBe(100);
|
||||
});
|
||||
|
||||
it('null quantity stays null after aggregation', () => {
|
||||
const db = openInMemoryForTest();
|
||||
const id = insertRecipe(db, recipe({
|
||||
ingredients: [{ position: 1, quantity: null, unit: null, name: 'Salz', note: null, raw_text: '', section_heading: null }]
|
||||
}));
|
||||
addRecipeToCart(db, id, null);
|
||||
const rows = listShoppingList(db).rows;
|
||||
expect(rows[0].total_quantity).toBeNull();
|
||||
expect(rows[0].unit_key).toBe('');
|
||||
});
|
||||
|
||||
it('counts unchecked rows in uncheckedCount', () => {
|
||||
const db = openInMemoryForTest();
|
||||
const id = insertRecipe(db, recipe({
|
||||
ingredients: [
|
||||
{ position: 1, quantity: 1, unit: 'Stk', name: 'Apfel', note: null, raw_text: '', section_heading: null },
|
||||
{ position: 2, quantity: 1, unit: 'Stk', name: 'Birne', note: null, raw_text: '', section_heading: null }
|
||||
]
|
||||
}));
|
||||
addRecipeToCart(db, id, null);
|
||||
expect(listShoppingList(db).uncheckedCount).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user