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.
This commit is contained in:
gzor
2026-05-25 17:59:54 +02:00
committed by GitHub
parent 0257e4e71e
commit 352f94612d
2 changed files with 27 additions and 9 deletions
@@ -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();
@@ -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 (
<BagCard key={bag.id} bag={bag} bagItems={bagItems} totalWeight={totalWeight} pct={pct} tripId={tripId} tripMembers={tripMembers} canEdit={canEdit} onDelete={() => 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 (
<div style={{ marginBottom: 14, opacity: 0.6 }}>
@@ -1342,7 +1346,7 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0,
<div style={{ borderTop: '1px solid var(--border-secondary)', paddingTop: 10, marginTop: 6 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, fontWeight: 700, color: 'var(--text-primary)' }}>
<span>{t('packing.totalWeight')}</span>
<span>{(() => { const w = items.reduce((s, i) => s + (i.weight_grams || 0), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()}</span>
<span>{(() => { const w = items.reduce((s, i) => s + itemWeight(i), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()}</span>
</div>
</div>
@@ -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 (
<BagCard key={bag.id} bag={bag} bagItems={bagItems} totalWeight={totalWeight} pct={pct} tripId={tripId} tripMembers={tripMembers} canEdit={canEdit} onDelete={() => 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 (
<div style={{ marginBottom: 16, opacity: 0.6 }}>
@@ -1411,7 +1415,7 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0,
<div style={{ borderTop: '1px solid var(--border-secondary)', paddingTop: 12, marginTop: 8 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 14, fontWeight: 700, color: 'var(--text-primary)' }}>
<span>{t('packing.totalWeight')}</span>
<span>{(() => { const w = items.reduce((s, i) => s + (i.weight_grams || 0), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()}</span>
<span>{(() => { const w = items.reduce((s, i) => s + itemWeight(i), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()}</span>
</div>
</div>