From 1b1566e6d7098380831c644ed9ee58617d750e5d Mon Sep 17 00:00:00 2001 From: Piotr Oleszczyk Date: Sat, 28 Feb 2026 23:07:37 +0100 Subject: [PATCH] feat(frontend): group products by category with ownership filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace category filter dropdown with client-side grouping and a 3-way ownership toggle (All / Owned / Not owned). Products are grouped by category with header rows as visual dividers, sorted brand → name within each group. Category column removed (redundant with headings). Backend: GET /products now returns ProductWithInventory so inventory data is available for ownership filtering (bulk-loaded in one query). Co-Authored-By: Claude Sonnet 4.6 --- backend/innercontext/api/products.py | 22 +++- frontend/src/routes/products/+page.server.ts | 7 +- frontend/src/routes/products/+page.svelte | 129 +++++++++++-------- 3 files changed, 100 insertions(+), 58 deletions(-) diff --git a/backend/innercontext/api/products.py b/backend/innercontext/api/products.py index efd4c6d..6dff032 100644 --- a/backend/innercontext/api/products.py +++ b/backend/innercontext/api/products.py @@ -172,7 +172,7 @@ class InventoryUpdate(SQLModel): # --------------------------------------------------------------------------- -@router.get("", response_model=list[ProductPublic]) +@router.get("", response_model=list[ProductWithInventory]) def list_products( category: Optional[ProductCategory] = None, brand: Optional[str] = None, @@ -205,7 +205,25 @@ def list_products( ) ] - return products + # Bulk-load inventory for all products in one query + product_ids = [p.id for p in products] + inventory_rows = ( + session.exec( + select(ProductInventory).where(ProductInventory.product_id.in_(product_ids)) + ).all() + if product_ids + else [] + ) + inv_by_product: dict = {} + for inv in inventory_rows: + inv_by_product.setdefault(inv.product_id, []).append(inv) + + results = [] + for p in products: + r = ProductWithInventory.model_validate(p, from_attributes=True) + r.inventory = inv_by_product.get(p.id, []) + results.append(r) + return results @router.post("", response_model=ProductPublic, status_code=201) diff --git a/frontend/src/routes/products/+page.server.ts b/frontend/src/routes/products/+page.server.ts index 6bb8800..5248dcf 100644 --- a/frontend/src/routes/products/+page.server.ts +++ b/frontend/src/routes/products/+page.server.ts @@ -1,8 +1,7 @@ import { getProducts } from '$lib/api'; import type { PageServerLoad } from './$types'; -export const load: PageServerLoad = async ({ url }) => { - const category = url.searchParams.get('category') ?? undefined; - const products = await getProducts({ category }); - return { products, category }; +export const load: PageServerLoad = async () => { + const products = await getProducts(); + return { products }; }; diff --git a/frontend/src/routes/products/+page.svelte b/frontend/src/routes/products/+page.svelte index 6e1bd37..a8ab046 100644 --- a/frontend/src/routes/products/+page.svelte +++ b/frontend/src/routes/products/+page.svelte @@ -1,5 +1,6 @@ Products — innercontext @@ -33,28 +60,21 @@

Products

-

{data.products.length} products

+

{totalCount} products

-
- Filter by category: - +
+ {#each (['all', 'owned', 'unowned'] as OwnershipFilter[]) as f (f)} + + {/each}
@@ -63,42 +83,47 @@ Name Brand - Category Targets Time - {#each data.products as product (product.id)} - - - - {product.name} - - - {product.brand} - - {product.category.replace(/_/g, ' ')} - - -
- {#each product.targets.slice(0, 3) as t (t)} - {t.replace(/_/g, ' ')} - {/each} - {#if product.targets.length > 3} - +{product.targets.length - 3} - {/if} -
-
- {product.recommended_time} -
- {:else} + {#if totalCount === 0} - + No products found. - {/each} + {:else} + {#each groupedProducts as [category, products] (category)} + + + {category.replace(/_/g, ' ')} + + + {#each products as product (product.id)} + + + + {product.name} + + + {product.brand} + +
+ {#each product.targets.slice(0, 3) as t (t)} + {t.replace(/_/g, ' ')} + {/each} + {#if product.targets.length > 3} + +{product.targets.length - 3} + {/if} +
+
+ {product.recommended_time} +
+ {/each} + {/each} + {/if}