From 1a5e6a303ef6ccdf4a6fdfb2f2ffdfea853c065b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Apr 2026 18:04:27 +0200 Subject: [PATCH] feat: add quantity support to totals, UI, and thread resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - totals.service: multiply weight/cost sums by quantity in category and global totals - setup.service: multiply by quantity in getAllSetups SQL subqueries; expose quantity in getSetupWithItems item list - thread.service: explicitly pass quantity: 1 when inserting resolved item - ItemForm: add Quantity number input (min=1, default=1) after price field - ItemCard: show ×N badge next to item name when quantity > 1 - CollectionView: pass quantity prop to ItemCard in both filtered and grouped views - $setupId.tsx: pass quantity to ItemCard; multiply by quantity in client-side per-setup totals - WeightSummaryCard: multiply by quantity in all chart and legend weight calculations - useItems / useSetups: add quantity to ItemWithCategory / SetupItemWithCategory interfaces Co-Authored-By: Claude Sonnet 4.6 --- src/client/components/CollectionView.tsx | 2 ++ src/client/components/ItemCard.tsx | 15 ++++++++--- src/client/components/ItemForm.tsx | 28 +++++++++++++++++++++ src/client/components/WeightSummaryCard.tsx | 25 +++++++++++++----- src/client/hooks/useItems.ts | 1 + src/client/hooks/useSetups.ts | 1 + src/client/routes/setups/$setupId.tsx | 13 ++++++---- src/server/services/setup.service.ts | 5 ++-- src/server/services/thread.service.ts | 1 + src/server/services/totals.service.ts | 8 +++--- 10 files changed, 79 insertions(+), 20 deletions(-) diff --git a/src/client/components/CollectionView.tsx b/src/client/components/CollectionView.tsx index b337d23..f3f4d51 100644 --- a/src/client/components/CollectionView.tsx +++ b/src/client/components/CollectionView.tsx @@ -224,6 +224,7 @@ export function CollectionView() { name={item.name} weightGrams={item.weightGrams} priceCents={item.priceCents} + quantity={item.quantity} categoryName={item.categoryName} categoryIcon={item.categoryIcon} imageFilename={item.imageFilename} @@ -257,6 +258,7 @@ export function CollectionView() { name={item.name} weightGrams={item.weightGrams} priceCents={item.priceCents} + quantity={item.quantity} categoryName={categoryName} categoryIcon={categoryIcon} imageFilename={item.imageFilename} diff --git a/src/client/components/ItemCard.tsx b/src/client/components/ItemCard.tsx index 03f4b49..e6383ee 100644 --- a/src/client/components/ItemCard.tsx +++ b/src/client/components/ItemCard.tsx @@ -8,6 +8,7 @@ interface ItemCardProps { name: string; weightGrams: number | null; priceCents: number | null; + quantity?: number; categoryName: string; categoryIcon: string; imageFilename: string | null; @@ -22,6 +23,7 @@ export function ItemCard({ name, weightGrams, priceCents, + quantity, categoryName, categoryIcon, imageFilename, @@ -122,9 +124,16 @@ export function ItemCard({ )}
-

- {name} -

+
+

+ {name} +

+ {quantity != null && quantity > 1 && ( + + ×{quantity} + + )} +
{weightGrams != null && ( diff --git a/src/client/components/ItemForm.tsx b/src/client/components/ItemForm.tsx index fbb2ec2..dc2d127 100644 --- a/src/client/components/ItemForm.tsx +++ b/src/client/components/ItemForm.tsx @@ -13,6 +13,7 @@ interface FormData { name: string; weightGrams: string; priceDollars: string; + quantity: number; categoryId: number; notes: string; productUrl: string; @@ -23,6 +24,7 @@ const INITIAL_FORM: FormData = { name: "", weightGrams: "", priceDollars: "", + quantity: 1, categoryId: 1, notes: "", productUrl: "", @@ -49,6 +51,7 @@ export function ItemForm({ mode, itemId }: ItemFormProps) { weightGrams: item.weightGrams != null ? String(item.weightGrams) : "", priceDollars: item.priceCents != null ? (item.priceCents / 100).toFixed(2) : "", + quantity: item.quantity ?? 1, categoryId: item.categoryId, notes: item.notes ?? "", productUrl: item.productUrl ?? "", @@ -98,6 +101,7 @@ export function ItemForm({ mode, itemId }: ItemFormProps) { priceCents: form.priceDollars ? Math.round(Number(form.priceDollars) * 100) : undefined, + quantity: form.quantity, categoryId: form.categoryId, notes: form.notes.trim() || undefined, productUrl: form.productUrl.trim() || undefined, @@ -202,6 +206,30 @@ export function ItemForm({ mode, itemId }: ItemFormProps) { )}
+ {/* Quantity */} +
+ + + setForm((f) => ({ + ...f, + quantity: Math.max(1, Number(e.target.value) || 1), + })) + } + className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + /> +
+ {/* Category */}