diff --git a/client/src/components/Packing/PackingListPanelBagModal.tsx b/client/src/components/Packing/PackingListPanelBagModal.tsx
index fb638eb74..650ce8632 100644
--- a/client/src/components/Packing/PackingListPanelBagModal.tsx
+++ b/client/src/components/Packing/PackingListPanelBagModal.tsx
@@ -1,15 +1,18 @@
import { X, Plus } from 'lucide-react'
import type { PackingState } from './usePackingListPanel'
-import { bagFillPct, itemWeight } from './packingListPanel.helpers'
+import { bagFillPct, countsTowardsMyLoad, itemWeight } from './packingListPanel.helpers'
import { BagCard } from './PackingListPanelBagCard'
export function BagModal(S: PackingState) {
const {
- setShowBagModal, t, bags, items, tripId, tripMembers, canEdit, handleDeleteBag, handleUpdateBag, handleSetBagMembers,
+ setShowBagModal, t, bags, items, tripId, tripMembers, canEdit, currentUserId, handleDeleteBag, handleUpdateBag, handleSetBagMembers,
showAddBag, setShowAddBag, newBagName, setNewBagName, handleCreateBag,
} = S
+ // These numbers describe what you are carrying. An item someone shared with you stays
+ // in your list, but they are the one bringing it, so it is not your weight (#1767).
+ const myItems = items.filter(i => countsTowardsMyLoad(i, currentUserId))
// Reference for bags without a limit of their own — computed once instead of per bag.
- const heaviestBagWeight = Math.max(...bags.map(b => items.filter(i => i.bag_id === b.id).reduce((s, i) => s + itemWeight(i), 0)), 1)
+ const heaviestBagWeight = Math.max(...bags.map(b => myItems.filter(i => i.bag_id === b.id).reduce((s, i) => s + itemWeight(i), 0)), 1)
return (
setShowBagModal(false)}>
@@ -21,7 +24,7 @@ export function BagModal(S: PackingState) {
{bags.map(bag => {
- const bagItems = items.filter(i => i.bag_id === bag.id)
+ const bagItems = myItems.filter(i => i.bag_id === bag.id)
const totalWeight = bagItems.reduce((sum, i) => sum + itemWeight(i), 0)
const pct = bagFillPct(totalWeight, bag.weight_limit_grams, heaviestBagWeight)
return (
@@ -31,7 +34,7 @@ export function BagModal(S: PackingState) {
{/* Unassigned */}
{(() => {
- const unassigned = items.filter(i => !i.bag_id)
+ const unassigned = myItems.filter(i => !i.bag_id)
const unassignedWeight = unassigned.reduce((s, i) => s + itemWeight(i), 0)
if (unassigned.length === 0) return null
return (
@@ -52,7 +55,7 @@ export function BagModal(S: PackingState) {
@@ -17,7 +20,7 @@ export function BagSidebar(S: PackingState) {
{bags.map(bag => {
- const bagItems = items.filter(i => i.bag_id === bag.id)
+ const bagItems = myItems.filter(i => i.bag_id === bag.id)
const totalWeight = bagItems.reduce((sum, i) => sum + itemWeight(i), 0)
const pct = bagFillPct(totalWeight, bag.weight_limit_grams, heaviestBagWeight)
return (
@@ -27,7 +30,7 @@ export function BagSidebar(S: PackingState) {
{/* Unassigned */}
{(() => {
- const unassigned = items.filter(i => !i.bag_id)
+ const unassigned = myItems.filter(i => !i.bag_id)
const unassignedWeight = unassigned.reduce((s, i) => s + itemWeight(i), 0)
if (unassigned.length === 0) return null
return (
@@ -48,7 +51,7 @@ export function BagSidebar(S: PackingState) {
{t('packing.totalWeight')}
- {(() => { const w = items.reduce((s, i) => s + itemWeight(i), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()}
+ {(() => { const w = myItems.reduce((s, i) => s + itemWeight(i), 0); return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g` })()}
diff --git a/client/src/components/Packing/packingListPanel.helpers.test.ts b/client/src/components/Packing/packingListPanel.helpers.test.ts
index 99786e95d..d3d33bc5b 100644
--- a/client/src/components/Packing/packingListPanel.helpers.test.ts
+++ b/client/src/components/Packing/packingListPanel.helpers.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
-import { katColor, itemWeight, bagFillPct, parseCsvLine, parseImportLines } from './packingListPanel.helpers'
+import { katColor, itemWeight, bagFillPct, countsTowardsMyLoad, parseCsvLine, parseImportLines } from './packingListPanel.helpers'
import { KAT_COLORS } from './packingListPanel.constants'
describe('packingListPanel.helpers', () => {
@@ -46,6 +46,31 @@ describe('packingListPanel.helpers', () => {
})
})
+ describe('countsTowardsMyLoad', () => {
+ it('counts the common pool for everyone', () => {
+ // owner_id is stamped on every item, common ones included — filtering by it alone
+ // would shrink the group total to "only what I entered myself".
+ expect(countsTowardsMyLoad({ is_private: 0, owner_id: 2 }, 1)).toBe(true)
+ })
+
+ it('counts my own private items', () => {
+ expect(countsTowardsMyLoad({ is_private: 1, owner_id: 1 }, 1)).toBe(true)
+ })
+
+ it('leaves out an item somebody else shared with me', () => {
+ expect(countsTowardsMyLoad({ is_private: 1, owner_id: 2 }, 1)).toBe(false)
+ })
+
+ it('counts unowned legacy rows', () => {
+ expect(countsTowardsMyLoad({ is_private: 1, owner_id: null }, 1)).toBe(true)
+ })
+
+ it('filters nothing when the viewer is unknown', () => {
+ expect(countsTowardsMyLoad({ is_private: 1, owner_id: 2 }, null)).toBe(true)
+ expect(countsTowardsMyLoad({ is_private: 1, owner_id: 2 }, undefined)).toBe(true)
+ })
+ })
+
describe('bagFillPct', () => {
it('measures against the bag limit when there is one', () => {
expect(bagFillPct(5000, 20000, 99999)).toBe(25)
diff --git a/client/src/components/Packing/packingListPanel.helpers.ts b/client/src/components/Packing/packingListPanel.helpers.ts
index 6f030875c..80cdf59ca 100644
--- a/client/src/components/Packing/packingListPanel.helpers.ts
+++ b/client/src/components/Packing/packingListPanel.helpers.ts
@@ -14,6 +14,22 @@ export function katColor(kat: string, allCategories?: string[]): string {
export const itemWeight = (i: { weight_grams?: number | null; quantity?: number | null }): number =>
(i.weight_grams || 0) * (i.quantity || 1)
+/**
+ * Whether an item's weight is part of what *you* carry. An item shared through the
+ * "Shared with…" tier stays visible to its recipients, but the owner is the one bringing
+ * it — counting it for everyone it was shared with inflated their bags (#1767).
+ * Common items are the group pool and always count; so does anything unowned (legacy
+ * rows) and everything, if we don't know who is looking.
+ */
+export const countsTowardsMyLoad = (
+ i: { is_private?: number | boolean | null; owner_id?: number | null },
+ currentUserId?: number | null,
+): boolean => {
+ if (currentUserId == null) return true
+ if (!i.is_private) return true
+ return i.owner_id == null || i.owner_id === currentUserId
+}
+
/**
* How full a bag's bar reads. A bag with a weight limit is measured against that limit —
* that is the number an airline cares about. Without one there is nothing absolute to
diff --git a/client/src/mobile/screens/trip/tabs/MBagsSheet.tsx b/client/src/mobile/screens/trip/tabs/MBagsSheet.tsx
index f7a863149..451ac3eec 100644
--- a/client/src/mobile/screens/trip/tabs/MBagsSheet.tsx
+++ b/client/src/mobile/screens/trip/tabs/MBagsSheet.tsx
@@ -7,7 +7,7 @@ import { avatarSrc } from '../../../../utils/avatarSrc'
import type { PackingBag, PackingItem, TripMember } from '../../../../types'
import type { TripPlanner } from '../MTripShell'
import { formatWeight, packingItemWeight } from './listsModel'
-import { bagFillPct } from '../../../../components/Packing/packingListPanel.helpers'
+import { bagFillPct, countsTowardsMyLoad } from '../../../../components/Packing/packingListPanel.helpers'
export interface MBagsSheetProps {
planner: TripPlanner
@@ -17,6 +17,8 @@ export interface MBagsSheetProps {
items: PackingItem[]
tripMembers: TripMember[]
canEdit: boolean
+ /** Who is looking — decides whose load the weights describe (#1767). */
+ currentUserId?: number | null
onCreateBag: (name: string) => void
onUpdateBag: (bagId: number, data: PackingUpdateBagRequest) => void
onDeleteBag: (bagId: number) => void
@@ -29,17 +31,20 @@ export interface MBagsSheetProps {
* is the caller's business — this sheet just renders whatever bags it's given.
*/
export default function MBagsSheet({
- planner, open, onClose, bags, items, tripMembers, canEdit, onCreateBag, onUpdateBag, onDeleteBag, onSetBagMembers,
+ planner, open, onClose, bags, items, tripMembers, canEdit, currentUserId, onCreateBag, onUpdateBag, onDeleteBag, onSetBagMembers,
}: MBagsSheetProps) {
const { t } = planner
const [addingBag, setAddingBag] = useState(false)
const [newBagName, setNewBagName] = useState('')
- const unassigned = items.filter(i => !i.bag_id)
+ // These numbers describe what you are carrying. An item someone shared with you stays
+ // in your list, but they are the one bringing it, so it is not your weight (#1767).
+ const myItems = items.filter(i => countsTowardsMyLoad(i, currentUserId))
+ const unassigned = myItems.filter(i => !i.bag_id)
const unassignedWeight = unassigned.reduce((s, i) => s + packingItemWeight(i), 0)
- const totalWeight = items.reduce((s, i) => s + packingItemWeight(i), 0)
+ const totalWeight = myItems.reduce((s, i) => s + packingItemWeight(i), 0)
// Reference for bags without a limit of their own — computed once instead of per bag.
- const heaviestBagWeight = Math.max(...bags.map(b => items.filter(i => i.bag_id === b.id).reduce((s, i) => s + packingItemWeight(i), 0)), 1)
+ const heaviestBagWeight = Math.max(...bags.map(b => myItems.filter(i => i.bag_id === b.id).reduce((s, i) => s + packingItemWeight(i), 0)), 1)
const submitNewBag = () => {
if (!newBagName.trim()) return
@@ -54,7 +59,7 @@ export default function MBagsSheet({
{bags.map(bag => {
- const bagItems = items.filter(i => i.bag_id === bag.id)
+ const bagItems = myItems.filter(i => i.bag_id === bag.id)
const bagWeight = bagItems.reduce((s, i) => s + packingItemWeight(i), 0)
const pct = bagFillPct(bagWeight, bag.weight_limit_grams, heaviestBagWeight)
return (
diff --git a/client/src/mobile/screens/trip/tabs/MPackingListTab.tsx b/client/src/mobile/screens/trip/tabs/MPackingListTab.tsx
index a9d726a23..9cfcd2092 100644
--- a/client/src/mobile/screens/trip/tabs/MPackingListTab.tsx
+++ b/client/src/mobile/screens/trip/tabs/MPackingListTab.tsx
@@ -472,6 +472,7 @@ export default function MPackingListTab({ planner }: { planner: TripPlanner }) {
items={items}
tripMembers={tripMembers}
canEdit={canEdit}
+ currentUserId={currentUserId}
onCreateBag={createBag}
onUpdateBag={updateBag}
onDeleteBag={deleteBag}
diff --git a/client/tests/unit/mobile/trip/MBagsSheet.test.tsx b/client/tests/unit/mobile/trip/MBagsSheet.test.tsx
index b774738da..6893566b4 100644
--- a/client/tests/unit/mobile/trip/MBagsSheet.test.tsx
+++ b/client/tests/unit/mobile/trip/MBagsSheet.test.tsx
@@ -276,4 +276,43 @@ describe('MBagsSheet', () => {
expect(screen.queryByRole('button', { name: 'packing.setBagLimit' })).toBeNull()
})
+
+ // #1767: an item somebody shared with me is theirs to carry, so it must not land in my
+ // bag totals — while the common pool stays everyone's.
+ it('FE-MOB-BAGS-023: an item shared with me is not added to my bag weight', () => {
+ setup({
+ currentUserId: 1,
+ bags: [bag({ id: 1, name: 'Backpack' })],
+ items: [
+ item({ id: 1, name: 'Mine', bag_id: 1, weight_grams: 200, is_private: 1, owner_id: 1 } as Partial
),
+ item({ id: 2, name: 'Binoculars', bag_id: 1, weight_grams: 300, is_private: 1, owner_id: 2 } as Partial),
+ ],
+ })
+
+ expect(within(rowFor('Backpack')).getByText('200 g')).toBeInTheDocument()
+ expect(within(rowFor('Backpack')).getByText('1 admin.packingTemplates.items')).toBeInTheDocument()
+ })
+
+ it('FE-MOB-BAGS-024: common items still count for everyone', () => {
+ setup({
+ currentUserId: 1,
+ bags: [bag({ id: 1, name: 'Backpack' })],
+ items: [
+ item({ id: 1, name: 'First-aid kit', bag_id: 1, weight_grams: 500, is_private: 0, owner_id: 2 } as Partial),
+ ],
+ })
+
+ expect(within(rowFor('Backpack')).getByText('500 g')).toBeInTheDocument()
+ })
+
+ it('FE-MOB-BAGS-025: without a known viewer nothing is filtered out', () => {
+ setup({
+ bags: [bag({ id: 1, name: 'Backpack' })],
+ items: [
+ item({ id: 1, name: 'Binoculars', bag_id: 1, weight_grams: 300, is_private: 1, owner_id: 2 } as Partial),
+ ],
+ })
+
+ expect(within(rowFor('Backpack')).getByText('300 g')).toBeInTheDocument()
+ })
})