From 352f94612dda7b5ed86d31b65c7c8385390ec5aa Mon Sep 17 00:00:00 2001 From: gzor Date: Mon, 25 May 2026 17:59:54 +0200 Subject: [PATCH] fix(packing): multiply item weight by quantity in bag/total weight calcs (#898) Quantity now counts toward bag and total weights. Generalised to an itemWeight() helper used by every weight sum (bag totals + max, unassigned, grand total; sidebar + bag modal) with unit tests. --- .../Packing/PackingListPanel.test.tsx | 16 ++++++++++++++- .../components/Packing/PackingListPanel.tsx | 20 +++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/client/src/components/Packing/PackingListPanel.test.tsx b/client/src/components/Packing/PackingListPanel.test.tsx index 2e1414ec..2dbbb01c 100644 --- a/client/src/components/Packing/PackingListPanel.test.tsx +++ b/client/src/components/Packing/PackingListPanel.test.tsx @@ -8,7 +8,21 @@ import { useAuthStore } from '../../store/authStore'; import { useTripStore } from '../../store/tripStore'; import { resetAllStores, seedStore } from '../../../tests/helpers/store'; import { buildUser, buildTrip, buildPackingItem } from '../../../tests/helpers/factories'; -import PackingListPanel from './PackingListPanel'; +import PackingListPanel, { itemWeight } from './PackingListPanel'; + +describe('itemWeight (bag total weight calc)', () => { + it('FE-COMP-PACKING-030: multiplies unit weight by quantity', () => { + expect(itemWeight({ weight_grams: 120, quantity: 3 })).toBe(360); + }); + it('FE-COMP-PACKING-031: defaults quantity to 1 when missing', () => { + expect(itemWeight({ weight_grams: 250 })).toBe(250); + }); + it('FE-COMP-PACKING-032: contributes 0 when weight is missing or zero', () => { + expect(itemWeight({ quantity: 5 })).toBe(0); + expect(itemWeight({ weight_grams: 0, quantity: 5 })).toBe(0); + expect(itemWeight({})).toBe(0); + }); +}); beforeEach(() => { resetAllStores(); diff --git a/client/src/components/Packing/PackingListPanel.tsx b/client/src/components/Packing/PackingListPanel.tsx index 9311cbc1..267fd55a 100644 --- a/client/src/components/Packing/PackingListPanel.tsx +++ b/client/src/components/Packing/PackingListPanel.tsx @@ -69,6 +69,10 @@ function katColor(kat, allCategories) { interface PackingBag { id: number; trip_id: number; name: string; color: string; weight_limit_grams: number | null; user_id?: number | null; assigned_username?: string | null } +/** Weight an item contributes to a total: unit weight times quantity (defaults: 0 g, qty 1). */ +export const itemWeight = (i: { weight_grams?: number | null; quantity?: number | null }): number => + (i.weight_grams || 0) * (i.quantity || 1) + // ── Bag Card ────────────────────────────────────────────────────────────── interface BagCardProps { @@ -1311,8 +1315,8 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0, {bags.map(bag => { const bagItems = items.filter(i => i.bag_id === bag.id) - const totalWeight = bagItems.reduce((sum, i) => sum + (i.weight_grams || 0), 0) - const maxWeight = bag.weight_limit_grams || Math.max(...bags.map(b => items.filter(i => i.bag_id === b.id).reduce((s, i) => s + (i.weight_grams || 0), 0)), 1) + const totalWeight = bagItems.reduce((sum, i) => sum + itemWeight(i), 0) + const maxWeight = bag.weight_limit_grams || Math.max(...bags.map(b => items.filter(i => i.bag_id === b.id).reduce((s, i) => s + itemWeight(i), 0)), 1) const pct = Math.min(100, Math.round((totalWeight / maxWeight) * 100)) return ( handleDeleteBag(bag.id)} onUpdate={handleUpdateBag} onSetMembers={handleSetBagMembers} t={t} compact /> @@ -1322,7 +1326,7 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0, {/* Unassigned */} {(() => { const unassigned = items.filter(i => !i.bag_id) - const unassignedWeight = unassigned.reduce((s, i) => s + (i.weight_grams || 0), 0) + const unassignedWeight = unassigned.reduce((s, i) => s + itemWeight(i), 0) if (unassigned.length === 0) return null return (
@@ -1342,7 +1346,7 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0,
{t('packing.totalWeight')} - {(() => { const w = items.reduce((s, i) => s + (i.weight_grams || 0), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()} + {(() => { const w = items.reduce((s, i) => s + itemWeight(i), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()}
@@ -1380,8 +1384,8 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0, {bags.map(bag => { const bagItems = items.filter(i => i.bag_id === bag.id) - const totalWeight = bagItems.reduce((sum, i) => sum + (i.weight_grams || 0), 0) - const maxWeight = Math.max(...bags.map(b => items.filter(i => i.bag_id === b.id).reduce((s, i) => s + (i.weight_grams || 0), 0)), 1) + const totalWeight = bagItems.reduce((sum, i) => sum + itemWeight(i), 0) + const maxWeight = Math.max(...bags.map(b => items.filter(i => i.bag_id === b.id).reduce((s, i) => s + itemWeight(i), 0)), 1) const pct = Math.min(100, Math.round((totalWeight / maxWeight) * 100)) return ( handleDeleteBag(bag.id)} onUpdate={handleUpdateBag} onSetMembers={handleSetBagMembers} t={t} /> @@ -1391,7 +1395,7 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0, {/* Unassigned */} {(() => { const unassigned = items.filter(i => !i.bag_id) - const unassignedWeight = unassigned.reduce((s, i) => s + (i.weight_grams || 0), 0) + const unassignedWeight = unassigned.reduce((s, i) => s + itemWeight(i), 0) if (unassigned.length === 0) return null return (
@@ -1411,7 +1415,7 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0,
{t('packing.totalWeight')} - {(() => { const w = items.reduce((s, i) => s + (i.weight_grams || 0), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()} + {(() => { const w = items.reduce((s, i) => s + itemWeight(i), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()}