feat(shopping): clearCheckedItems + Orphan-Cleanup
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m13s
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 2m13s
This commit is contained in:
@@ -125,8 +125,66 @@ export function toggleCheck(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearCheckedItems(_db: Database.Database): void {
|
export function clearCheckedItems(db: Database.Database): void {
|
||||||
throw new Error('not implemented');
|
const tx = db.transaction(() => {
|
||||||
|
// Alle aggregierten Zeilen mit checked-Status holen, pro recipe_id gruppieren
|
||||||
|
// und Rezepte finden, deren Zeilen ALLE abgehakt sind.
|
||||||
|
const allRows = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT
|
||||||
|
cr.recipe_id,
|
||||||
|
LOWER(TRIM(i.name)) AS name_key,
|
||||||
|
LOWER(TRIM(COALESCE(i.unit, ''))) AS unit_key,
|
||||||
|
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 ingredient i ON i.recipe_id = cr.recipe_id`
|
||||||
|
)
|
||||||
|
.all() as { recipe_id: number; name_key: string; unit_key: string; checked: 0 | 1 }[];
|
||||||
|
|
||||||
|
const perRecipe = new Map<number, { total: number; checked: number }>();
|
||||||
|
for (const r of allRows) {
|
||||||
|
const e = perRecipe.get(r.recipe_id) ?? { total: 0, checked: 0 };
|
||||||
|
e.total += 1;
|
||||||
|
e.checked += r.checked;
|
||||||
|
perRecipe.set(r.recipe_id, e);
|
||||||
|
}
|
||||||
|
const toRemove: number[] = [];
|
||||||
|
for (const [id, e] of perRecipe) {
|
||||||
|
if (e.total > 0 && e.total === e.checked) toRemove.push(id);
|
||||||
|
}
|
||||||
|
for (const id of toRemove) {
|
||||||
|
db.prepare('DELETE FROM shopping_cart_recipe WHERE recipe_id = ?').run(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orphan-Checks raeumen: alle Check-Keys, die jetzt in KEINEM Cart-Rezept
|
||||||
|
// mehr vorkommen.
|
||||||
|
const activeKeys = db
|
||||||
|
.prepare(
|
||||||
|
`SELECT DISTINCT
|
||||||
|
LOWER(TRIM(i.name)) AS name_key,
|
||||||
|
LOWER(TRIM(COALESCE(i.unit, ''))) AS unit_key
|
||||||
|
FROM shopping_cart_recipe cr
|
||||||
|
JOIN ingredient i ON i.recipe_id = cr.recipe_id`
|
||||||
|
)
|
||||||
|
.all() as { name_key: string; unit_key: string }[];
|
||||||
|
const activeSet = new Set(activeKeys.map((k) => `${k.name_key} ${k.unit_key}`));
|
||||||
|
const allChecks = db
|
||||||
|
.prepare('SELECT name_key, unit_key FROM shopping_cart_check')
|
||||||
|
.all() as { name_key: string; unit_key: string }[];
|
||||||
|
const del = db.prepare(
|
||||||
|
'DELETE FROM shopping_cart_check WHERE name_key = ? AND unit_key = ?'
|
||||||
|
);
|
||||||
|
for (const c of allChecks) {
|
||||||
|
if (!activeSet.has(`${c.name_key} ${c.unit_key}`)) {
|
||||||
|
del.run(c.name_key, c.unit_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tx();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearCart(_db: Database.Database): void {
|
export function clearCart(_db: Database.Database): void {
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
removeRecipeFromCart,
|
removeRecipeFromCart,
|
||||||
listShoppingList,
|
listShoppingList,
|
||||||
setCartServings,
|
setCartServings,
|
||||||
toggleCheck
|
toggleCheck,
|
||||||
|
clearCheckedItems
|
||||||
} from '../../src/lib/server/shopping/repository';
|
} from '../../src/lib/server/shopping/repository';
|
||||||
import type { Recipe } from '../../src/lib/types';
|
import type { Recipe } from '../../src/lib/types';
|
||||||
|
|
||||||
@@ -231,3 +232,56 @@ describe('toggleCheck', () => {
|
|||||||
expect(rows[0].total_quantity).toBe(200);
|
expect(rows[0].total_quantity).toBe(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('clearCheckedItems', () => {
|
||||||
|
it('removes recipes where ALL rows are checked', () => {
|
||||||
|
const db = openInMemoryForTest();
|
||||||
|
const a = insertRecipe(db, recipe({
|
||||||
|
title: 'A',
|
||||||
|
ingredients: [{ position: 1, quantity: 1, unit: 'Stk', name: 'Apfel', note: null, raw_text: '', section_heading: null }]
|
||||||
|
}));
|
||||||
|
const b = insertRecipe(db, recipe({
|
||||||
|
title: 'B',
|
||||||
|
ingredients: [
|
||||||
|
{ position: 1, quantity: 1, unit: 'Stk', name: 'Birne', note: null, raw_text: '', section_heading: null },
|
||||||
|
{ position: 2, quantity: 1, unit: 'Stk', name: 'Salz', note: null, raw_text: '', section_heading: null }
|
||||||
|
]
|
||||||
|
}));
|
||||||
|
addRecipeToCart(db, a, null);
|
||||||
|
addRecipeToCart(db, b, null);
|
||||||
|
toggleCheck(db, 'apfel', 'stk', true);
|
||||||
|
toggleCheck(db, 'birne', 'stk', true);
|
||||||
|
// Salz aus B noch nicht abgehakt → B bleibt, A fliegt
|
||||||
|
clearCheckedItems(db);
|
||||||
|
const snap = listShoppingList(db);
|
||||||
|
expect(snap.recipes.map((r) => r.recipe_id)).toEqual([b]);
|
||||||
|
// Birne-Check bleibt, weil B noch im Cart und Birne noch aktiv
|
||||||
|
const birneRow = snap.rows.find((r) => r.name_key === 'birne');
|
||||||
|
expect(birneRow?.checked).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('purges orphan checks that no longer map to any cart recipe', () => {
|
||||||
|
const db = openInMemoryForTest();
|
||||||
|
const id = insertRecipe(db, recipe({
|
||||||
|
ingredients: [{ position: 1, quantity: 1, unit: 'Stk', name: 'Apfel', note: null, raw_text: '', section_heading: null }]
|
||||||
|
}));
|
||||||
|
addRecipeToCart(db, id, null);
|
||||||
|
toggleCheck(db, 'apfel', 'stk', true);
|
||||||
|
clearCheckedItems(db);
|
||||||
|
// Apfel-Check haengt jetzt an nichts mehr → muss aus der Tabelle raus sein
|
||||||
|
const row = db
|
||||||
|
.prepare('SELECT * FROM shopping_cart_check WHERE name_key = ?')
|
||||||
|
.get('apfel');
|
||||||
|
expect(row).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is a no-op when nothing is checked', () => {
|
||||||
|
const db = openInMemoryForTest();
|
||||||
|
const id = insertRecipe(db, recipe({
|
||||||
|
ingredients: [{ position: 1, quantity: 1, unit: 'Stk', name: 'Apfel', note: null, raw_text: '', section_heading: null }]
|
||||||
|
}));
|
||||||
|
addRecipeToCart(db, id, null);
|
||||||
|
clearCheckedItems(db);
|
||||||
|
expect(listShoppingList(db).recipes).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user